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..d123023a68 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, @@ -146,6 +147,7 @@ "context": true, "before": true, "beforeEach": true, - "qz": true + "qz": true, + "localforage": 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/.mergify.yml b/.mergify.yml index 5b0ec71b1c..eae959b8a0 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -14,7 +14,6 @@ pull_request_rules: - name: Automatic squash on CI success and review conditions: - status-success=Sider - - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk (frappe) - label!=dont-merge 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 2331217363..53ad56a948 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,7 +43,6 @@ matrix: 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 2279dc399d..7a5b1611b0 100644 --- a/cypress/integration/api.js +++ b/cypress/integration/api.js @@ -2,12 +2,12 @@ context('API Resources', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); }); it('Creates two Comments', () => { - cy.insert_doc('Comment', {comment_type: 'Comment', content: "hello"}); - cy.insert_doc('Comment', {comment_type: 'Comment', content: "world"}); + cy.insert_doc('Comment', { comment_type: 'Comment', content: "hello" }); + cy.insert_doc('Comment', { comment_type: 'Comment', content: "world" }); }); it('Lists the Comments', () => { diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 15e85976fc..3e12101532 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -2,11 +2,11 @@ context('Awesome Bar', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); }); beforeEach(() => { - cy.get('.navbar-header .navbar-home').click(); + cy.get('.navbar .navbar-home').click(); }); it('navigates to doctype list', () => { @@ -14,16 +14,16 @@ context('Awesome Bar', () => { cy.get('#navbar-search + ul').should('be.visible'); cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 }); - cy.get('h1').should('contain', 'To Do'); + cy.get('.title-text').should('contain', 'To Do'); - cy.location('hash').should('eq', '#List/ToDo/List'); + cy.location('pathname').should('eq', '/app/todo'); }); it('find text in doctype list', () => { cy.get('#navbar-search') .type('test in todo{downarrow}{enter}', { delay: 200 }); - cy.get('h1').should('contain', 'To Do'); + cy.get('.title-text').should('contain', 'To Do'); cy.get('[data-original-title="Name"] > .input-with-feedback') .should('have.value', '%test%'); @@ -33,7 +33,7 @@ context('Awesome Bar', () => { cy.get('#navbar-search') .type('new blog post{downarrow}{enter}', { delay: 200 }); - cy.get('.title-text:visible').should('have.text', 'New Blog Post 1'); + cy.get('.title-text:visible').should('have.text', 'New Blog Post'); }); it('calculates math expressions', () => { diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js index 4e05d864e6..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('/desk#workspace/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 edad759216..266d421e70 100644 --- a/cypress/integration/control_duration.js +++ b/cypress/integration/control_duration.js @@ -1,10 +1,10 @@ context('Control Duration', () => { before(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); }); - function get_dialog_with_duration(hide_days=0, hide_seconds=0) { + function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) { return cy.dialog({ title: 'Duration', fields: [{ @@ -22,11 +22,11 @@ context('Control Duration', () => { .first() .click(); cy.get('.duration-input[data-duration=days]') - .type(45, {force: true}) - .blur({force: true}); + .type(45, { force: true }) + .blur({ force: true }); cy.get('.duration-input[data-duration=minutes]') .type(30) - .blur({force: true}); + .blur({ force: true }); cy.get('.frappe-control[data-fieldname=duration] input').first().should('have.value', '45d 30m'); cy.get('.frappe-control[data-fieldname=duration] input').first().blur(); cy.get('.duration-picker').should('not.be.visible'); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 0dc7d5b88e..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('/desk#workspace/Website'); + cy.visit('/app/website'); }); beforeEach(() => { - cy.visit('/desk#workspace/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'); @@ -77,7 +74,7 @@ context('Control Link', () => { cy.get('.frappe-control[data-fieldname=link] .link-btn') .should('be.visible') .click(); - cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`); + cy.location('pathname').should('eq', `/app/todo/${todos[0]}`); }); }); }); diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js index e89ab2d3be..592ed87004 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('/desk#workspace/Website'); + cy.visit('/app/website'); }); function get_dialog_with_rating() { @@ -18,7 +18,7 @@ context('Control Rating', () => { get_dialog_with_rating().as('dialog'); cy.get('div.rating') - .children('i.fa') + .children('svg') .first() .click() .should('have.class', 'star-click'); @@ -33,11 +33,11 @@ context('Control Rating', () => { get_dialog_with_rating(); cy.get('div.rating') - .children('i.fa') + .children('svg') .first() .invoke('trigger', 'mouseenter') .should('have.class', 'star-hover') .invoke('trigger', 'mouseleave') .should('not.have.class', 'star-hover'); }); -}); \ No newline at end of file +}); diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js index 9bf01632bf..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('/desk#workspace/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 aa80afb59a..d33babb134 100644 --- a/cypress/integration/depends_on.js +++ b/cypress/integration/depends_on.js @@ -1,7 +1,7 @@ context('Depends On', () => { before(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', { name: 'Child Test Depends On', @@ -64,7 +64,7 @@ context('Depends On', () => { cy.fill_field('test_field', 'Some Value'); cy.get('button.primary-action').contains('Save').click(); cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible'); - cy.get('body').click(); + cy.hide_dialog(); cy.fill_field('test_field', 'Random value'); cy.get('button.primary-action').contains('Save').click(); cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible'); @@ -92,7 +92,7 @@ context('Depends On', () => { 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('.octicon-triangle-up').click(); + 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'); diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index f9f44675db..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('/desk#workspace/Website'); + cy.visit('/app'); }); function open_upload_dialog() { @@ -19,44 +19,36 @@ context('FileUploader', () => { 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-info').should('contain', 'example.json'); - cy.server(); - cy.route('POST', '/api/method/upload_file').as('upload_file'); - cy.get_open_dialog().find('.btn-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('a:contains("uploaded file")').click(); + 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'); }); it('should accept web links', () => { open_upload_dialog(); - cy.get_open_dialog().find('a:contains("web link")').click(); + 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 ef89a18e7d..9c63fe4e8b 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,56 +1,50 @@ context('Form', () => { before(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); }); }); it('create a new form', () => { - cy.visit('/desk#Form/ToDo/New ToDo 1'); + cy.visit('/app/todo/new'); 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.visit('/desk#List/ToDo'); - cy.location('hash').should('eq', '#List/ToDo/List'); - cy.get('h1').should('be.visible').and('contain', 'To Do'); + 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'); }); it('navigates between documents with child table list filters applied', () => { - cy.visit('/desk#List/Contact'); - cy.location('hash').should('eq', '#List/Contact/List'); - cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); - cy.get('.fieldname-select-area').should('exist'); - cy.get('.fieldname-select-area input').type('Number{enter}', { force: true }); + cy.visit('/app/contact'); + cy.add_filter(); cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true }); - cy.get('.filter-box .btn:contains("Apply")').click({ force: true }); - cy.visit('/desk#Form/Contact/Test Form Contact 3'); + cy.get('.filter-popover .apply-filters').click({ force: true }); + 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('.page-title .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('/desk#Form/Contact/New Contact 1'); + cy.visit('/app/contact/new'); cy.get('.frappe-control[data-fieldname="email_ids"]').as('table'); cy.get('@table').find('button.grid-add-row').click(); cy.get('.grid-body .rows [data-fieldname="email_id"]').click(); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js index b383f30bb8..8f6b79c1f4 100644 --- a/cypress/integration/grid_pagination.js +++ b/cypress/integration/grid_pagination.js @@ -1,24 +1,24 @@ context('Grid Pagination', () => { beforeEach(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); }); before(() => { cy.login(); - cy.visit('/desk#workspace/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"); }); }); it('creates pages for child table', () => { - cy.visit('/desk#Form/Contact/Test Contact'); + cy.visit('/app/contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.current-page-number').should('contain', '1'); cy.get('@table').find('.total-page-number').should('contain', '20'); cy.get('@table').find('.grid-body .grid-row').should('have.length', 50); }); it('goes to the next and previous page', () => { - cy.visit('/desk#Form/Contact/Test Contact'); + cy.visit('/app/contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.next-page').click(); cy.get('@table').find('.current-page-number').should('contain', '2'); @@ -27,21 +27,21 @@ context('Grid Pagination', () => { cy.get('@table').find('.current-page-number').should('contain', '1'); cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1'); }); - it('adds and deletes rows and changes page', ()=> { - cy.visit('/desk#Form/Contact/Test Contact'); + it('adds and deletes rows and changes page', () => { + cy.visit('/app/contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('button.grid-add-row').click(); cy.get('@table').find('.grid-body .row-index').should('contain', 1001); cy.get('@table').find('.current-page-number').should('contain', '21'); cy.get('@table').find('.total-page-number').should('contain', '21'); - cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true}); + cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true }); cy.get('@table').find('button.grid-remove-rows').click(); cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000); cy.get('@table').find('.current-page-number').should('contain', '20'); cy.get('@table').find('.total-page-number').should('contain', '20'); }); // it('deletes all rows', ()=> { - // cy.visit('/desk#Form/Contact/Test Contact'); + // cy.visit('/app/contact/Test Contact'); // cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); // cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); // cy.get('@table').find('button.grid-remove-all-rows').click(); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 0d26ca90a2..633d1335ab 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -1,30 +1,31 @@ context('List View', () => { before(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); }); it('enables "Actions" button', () => { - const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete']; + const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; cy.go_to_list('ToDo'); cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); - cy.get('.btn.btn-primary.btn-sm.dropdown-toggle').contains('Actions').should('be.visible').click(); - cy.get('.dropdown-menu li:visible').should('have.length', 7).each((el, index) => { + cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); + cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => { cy.wrap(el).contains(actions[index]); }).then((elements) => { - cy.server(); - cy.route({ + cy.intercept({ method: 'POST', - url:'api/method/frappe.model.workflow.bulk_workflow_approval' + 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' + 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 47f8efe94b..52512b911e 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -1,36 +1,36 @@ context('List View Settings', () => { beforeEach(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); }); it('Default settings', () => { - cy.visit('/desk#List/DocType/List'); + cy.visit('/app/List/DocType/List'); cy.get('.list-count').should('contain', "20 of"); - cy.get('.sidebar-stat').should('contain', "Tags"); + cy.get('.list-stats').should('contain', "Tags"); }); it('disable count and sidebar stats then verify', () => { cy.wait(300); - cy.visit('/desk#List/DocType/List'); + cy.visit('/app/List/DocType/List'); cy.wait(300); cy.get('.list-count').should('contain', "20 of"); - cy.get('button').contains('Menu').click(); - cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click(); - cy.get('.modal-dialog').should('contain', 'Settings'); + cy.get('.menu-btn-group button').click(); + 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}); + cy.get('input[data-fieldname="disable_count"]').check({ force: true }); + cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true }); cy.get('button').filter(':visible').contains('Save').click(); - cy.reload(); + cy.reload({ force: true }); cy.get('.list-count').should('be.empty'); - cy.get('.list-sidebar .sidebar-stat').should('not.exist'); + cy.get('.list-sidebar .list-tags').should('not.exist'); - cy.get('button').contains('Menu').click({force: true}); - cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click(); - cy.get('.modal-dialog').should('contain', 'Settings'); - cy.get('input[data-fieldname="disable_count"]').uncheck({force: true}); - cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({force: true}); + cy.get('.menu-btn-group button').click({ force: true }); + 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 861377444c..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', () => { @@ -11,13 +11,13 @@ context('Login', () => { it('validates password', () => { cy.get('#login_email').type('Administrator'); - cy.get('.btn-login').click(); + cy.get('.btn-login:visible').click(); cy.location('pathname').should('eq', '/login'); }); it('validates email', () => { cy.get('#login_password').type('qwe'); - cy.get('.btn-login').click(); + cy.get('.btn-login:visible').click(); cy.location('pathname').should('eq', '/login'); }); @@ -25,8 +25,8 @@ context('Login', () => { cy.get('#login_email').type('Administrator'); cy.get('#login_password').type('qwer'); - cy.get('.btn-login').click(); - cy.get('.page-card-head').contains('Invalid Login. Try again.'); + cy.get('.btn-login:visible').click(); + cy.get('.btn-login:visible').contains('Invalid Login. Try again.'); cy.location('pathname').should('eq', '/login'); }); @@ -34,8 +34,8 @@ context('Login', () => { cy.get('#login_email').type('Administrator'); cy.get('#login_password').type(Cypress.config('adminPassword')); - cy.get('.btn-login').click(); - cy.location('pathname').should('eq', '/desk'); + cy.get('.btn-login:visible').click(); + cy.location('pathname').should('eq', '/app'); cy.window().its('frappe.session.user').should('eq', 'Administrator'); }); @@ -60,7 +60,7 @@ context('Login', () => { cy.get('#login_email').type('Administrator'); cy.get('#login_password').type(Cypress.config('adminPassword')); - cy.get('.btn-login').click(); + cy.get('.btn-login:visible').click(); // verify redirected location and url params after login cy.url().should('include', '/me?' + payload.toString().replace('+', '%20')); diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js index 5581a20edc..e2a1c3fc79 100644 --- a/cypress/integration/query_report.js +++ b/cypress/integration/query_report.js @@ -1,33 +1,33 @@ context('Query Report', () => { before(() => { cy.login(); - cy.visit('/desk#workspace/Website'); + cy.visit('/app/website'); }); it('add custom column in report', () => { - cy.visit('/desk#query-report/Permitted Documents For User'); + cy.visit('/app/query-report/Permitted Documents For User'); - cy.get('div[class="page-form flex"]', {timeout: 60000}).should('have.length', 1).then(()=>{ + cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => { cy.get('#page-query-report input[data-fieldname="user"]').as('input'); - cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }); - + cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur(); + cy.wait(300); cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test'); cy.get('@input-test').focus().type('Role', { delay: 100 }).blur(); cy.get('.datatable').should('exist'); - cy.get('button').contains('Menu').click({force: true}); - cy.get('.dropdown-menu li').contains('Add Column').click({force: true}); + cy.get('.menu-btn-group button').click({ force: true }); + cy.get('.dropdown-menu li').contains('Add Column').click({ force: true }); cy.get('.modal-dialog').should('contain', 'Add Column'); - cy.get('select[data-fieldname="doctype"]').select("Role", {force: true}); - cy.get('select[data-fieldname="field"]').select("Role Name", {force: true}); - cy.get('select[data-fieldname="insert_after"]').select("Name", {force: true}); - cy.get('button').contains('Submit').click({force: true}); - cy.get('button').contains('Menu').click({force: true}); - cy.get('.dropdown-menu li').contains('Save').click({force: true}); + cy.get('select[data-fieldname="doctype"]').select("Role", { force: true }); + cy.get('select[data-fieldname="field"]').select("Role Name", { force: true }); + cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true }); + cy.get('button').contains('Submit').click({ force: true }); + cy.get('.menu-btn-group button').click({ force: true }); + cy.get('.dropdown-menu li').contains('Save').click({ force: true }); cy.get('.modal-dialog').should('contain', 'Save Report'); - cy.get('input[data-fieldname="report_name"]').type("Test Report", {delay:100, force: true}); - cy.get('button').contains('Submit').click({timeout:1000, force: true}); + cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true }); + cy.get('button').contains('Submit').click({ timeout: 1000, force: true }); }); }); }); \ No newline at end of file diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index a0f8cc3621..7236200741 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -4,17 +4,17 @@ context('Recorder', () => { }); it('Navigate to Recorder', () => { - cy.visit('/desk#workspace/Website'); + cy.visit('/app'); cy.awesomebar('recorder'); - cy.get('h1').should('contain', 'Recorder'); - cy.location('hash').should('eq', '#recorder'); + cy.get('h3').should('contain', 'Recorder'); + cy.url().should('include', '/recorder/detail'); }); it('Recorder Empty State', () => { - cy.visit('/desk#recorder'); + 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'); @@ -24,53 +24,49 @@ context('Recorder', () => { }); it('Recorder Start', () => { - cy.visit('/desk#recorder'); + cy.visit('/app/recorder'); cy.get('.primary-action').should('contain', 'Start').click(); - cy.get('.indicator').should('contain', 'Active').should('have.class', 'green'); + cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green'); cy.get('.msg-box').should('contain', 'No Requests'); - cy.server(); - cy.visit('/desk#List/DocType/List'); - cy.route('POST', '/api/method/frappe.desk.reportview.get').as('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.visit('/desk#recorder'); + 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.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('/desk#recorder'); + cy.visit('/app/recorder'); cy.get('.primary-action').should('contain', 'Start').click(); - cy.server(); - cy.visit('/desk#List/DocType/List'); - cy.route('POST', '/api/method/frappe.desk.reportview.get').as('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 '); - // temporarily commenting out theses tests as they seem to be - // randomly failing maybe due a backround event + cy.visit('/app/recorder'); - // cy.visit('/desk#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 ac70c44345..80e6387d99 100644 --- a/cypress/integration/relative_time_filters.js +++ b/cypress/integration/relative_time_filters.js @@ -4,46 +4,44 @@ context('Relative Timeframe', () => { }); before(() => { cy.login(); - cy.visit('/desk#workspace/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('/desk#List/ToDo/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').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.get('.remove-filter.btn').click(); + cy.clear_filters(); cy.wait('@save_user_settings'); }); it('sets relative timespan filter for next week and filters list', () => { - cy.visit('/desk#List/ToDo/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.btn').click(); + cy.clear_filters(); cy.wait('@save_user_settings'); }); }); diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index deeef6bdd5..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('/desk#workspace/Website'); + cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); cy.insert_doc(doctype_name, { @@ -16,15 +16,14 @@ 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.visit(`/desk#List/${doctype_name}/Report`); + 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'); let cell = cy.get('.dt-row-0 > .dt-cell--col-4'); // select the cell cell.dblclick(); - cell.find('input[data-fieldname="enabled"]').check({force: true}); + cell.find('input[data-fieldname="enabled"]').check({ force: true }); cy.get('.dt-row-0 > .dt-cell--col-5').click(); cy.wait('@value-update'); cy.get('@doc').then(doc => { diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index e75baf05f1..8b83a0d914 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -13,15 +13,14 @@ context('Table MultiSelect', () => { cy.get('input[data-fieldname="users"]').focus().as('input'); cy.get('input[data-fieldname="users"] + ul').should('be.visible'); cy.get('@input').type('test{enter}', { delay: 100 }); - cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value') - .first().as('selected-value'); + cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value .btn-link-to-form') + .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'); }); @@ -46,6 +45,6 @@ context('Table MultiSelect', () => { cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value').as('existing_value'); cy.get('@existing_value').find('.btn-link-to-form').click(); - cy.location('hash').should('contain', 'Form/User/test@erpnext.com'); + cy.location('pathname').should('contain', '/user/test@erpnext.com'); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 3e54a9cd4c..1964b96d70 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -244,14 +244,14 @@ Cypress.Commands.add('awesomebar', text => { }); Cypress.Commands.add('new_form', doctype => { - let route = `Form/${doctype}/New ${doctype} 1`; - cy.visit(`/desk#${route}`); - cy.get('body').should('have.attr', 'data-route', route); + 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'); }); Cypress.Commands.add('go_to_list', doctype => { - cy.visit(`/desk#List/${doctype}/List`); + cy.visit(`/app/list/${doctype}/list`); }); Cypress.Commands.add('clear_cache', () => { @@ -275,9 +275,8 @@ Cypress.Commands.add('get_open_dialog', () => { }); Cypress.Commands.add('hide_dialog', () => { - cy.get_open_dialog() - .find('.btn-modal-close') - .click(); + cy.wait(300); + cy.get_open_dialog().find('.btn-modal-close').click(); cy.get('.modal:visible').should('not.exist'); }); @@ -307,4 +306,21 @@ Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { return res.body.data; }); }); -}); \ No newline at end of file +}); + +Cypress.Commands.add('add_filter', () => { + cy.get('.filter-section .filter-button').click(); + cy.wait(300); + cy.get('.filter-popover').should('exist'); +}); + +Cypress.Commands.add('clear_filters', () => { + cy.get('.filter-section .filter-button').click(); + 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 992f99131f..e137f8ed22 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 @@ -466,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**. @@ -492,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: @@ -514,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 = [] @@ -964,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: @@ -1632,7 +1635,7 @@ def log_error(message=None, title=_("Error")): method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): - html = '{doctype_local} {name}' + html = '{doctype_local} {name}' return html.format( doctype=doctype, name=name, diff --git a/frappe/app.py b/frappe/app.py index adf2bfa8c9..29ef69ef2d 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -181,6 +181,9 @@ def make_form_dict(request): else: args = request.form or request.args + if not isinstance(args, dict): + frappe.throw("Invalid request arguments") + try: frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \ for k, v in iteritems(args) }) diff --git a/frappe/auth.py b/frappe/auth.py index 6d51629c58..2e0ec681d2 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -173,7 +173,7 @@ class LoginManager: frappe.local.cookie_manager.set_cookie("system_user", "yes") if not resume: frappe.local.response['message'] = 'Logged In' - frappe.local.response["home_page"] = "/desk" + frappe.local.response["home_page"] = "/app" if not resume: frappe.response["full_name"] = self.full_name diff --git a/frappe/automation/desk_page/tools/tools.json b/frappe/automation/desk_page/tools/tools.json deleted file mode 100644 index 3fbaf62d02..0000000000 --- a/frappe/automation/desk_page/tools/tools.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "cards": [ - { - "hidden": 0, - "label": "Tools", - "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" - }, - { - "hidden": 0, - "label": "Email", - "links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Automation", - "links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]" - }, - { - "hidden": 0, - "label": "Event Streaming", - "links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]" - } - ], - "category": "Administration", - "charts": [], - "creation": "2020-03-02 14:53:24.980279", - "developer_mode_only": 0, - "disable_user_customization": 0, - "docstatus": 0, - "doctype": "Desk Page", - "extends_another_page": 0, - "hide_custom": 0, - "idx": 0, - "is_standard": 1, - "label": "Tools", - "modified": "2020-07-21 19:32:18.480700", - "modified_by": "Administrator", - "module": "Automation", - "name": "Tools", - "owner": "Administrator", - "pin_to_bottom": 0, - "pin_to_top": 0, - "shortcuts": [ - { - "label": "ToDo", - "link_to": "ToDo", - "type": "DocType" - }, - { - "label": "Note", - "link_to": "Note", - "type": "DocType" - }, - { - "label": "File", - "link_to": "File", - "type": "DocType" - }, - { - "label": "Assignment Rule", - "link_to": "Assignment Rule", - "type": "DocType" - }, - { - "label": "Auto Repeat", - "link_to": "Auto Repeat", - "type": "DocType" - } - ] -} \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index d54ae8d62c..7028ac486d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', { refresh: function(frm) { // auto repeat message if (frm.is_new()) { - let customize_form_link = `${__('Customize Form')}`; + let customize_form_link = `${__('Customize Form')}`; frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link])); } @@ -106,8 +106,9 @@ frappe.auto_repeat.render_schedule = function(frm) { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { - schedule_details : r.message || [] - }) + schedule_details: r.message || [] + }), + __('Auto Repeat Schedule') ); frm.dashboard.show(); }); diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 830af68de7..281e699640 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -15,6 +15,8 @@ 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} @@ -328,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) diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json new file mode 100644 index 0000000000..4a0835657b --- /dev/null +++ b/frappe/automation/workspace/tools/tools.json @@ -0,0 +1,229 @@ +{ + "category": "Administration", + "charts": [], + "creation": "2020-03-02 14:53:24.980279", + "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": "Tools", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Tools", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "To Do", + "link_to": "ToDo", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Calendar", + "link_to": "Event", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Note", + "link_to": "Note", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Files", + "link_to": "File", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Activity", + "link_to": "activity", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Group", + "link_to": "Email Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Automation", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Assignment Rule", + "link_to": "Assignment Rule", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Milestone", + "link_to": "Milestone", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Auto Repeat", + "link_to": "Auto Repeat", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Event Streaming", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Producer", + "link_to": "Event Producer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Consumer", + "link_to": "Event Consumer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Update Log", + "link_to": "Event Update Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Event Sync Log", + "link_to": "Event Sync Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Document Type Mapping", + "link_to": "Document Type Mapping", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2020-12-01 13:38:39.950350", + "modified_by": "Administrator", + "module": "Automation", + "name": "Tools", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "label": "ToDo", + "link_to": "ToDo", + "type": "DocType" + }, + { + "label": "Note", + "link_to": "Note", + "type": "DocType" + }, + { + "label": "File", + "link_to": "File", + "type": "DocType" + }, + { + "label": "Assignment Rule", + "link_to": "Assignment Rule", + "type": "DocType" + }, + { + "label": "Auto Repeat", + "link_to": "Auto Repeat", + "type": "DocType" + } + ] +} \ No newline at end of file diff --git a/frappe/boot.py b/frappe/boot.py index 5b1a1bf573..0dfcb8d1b4 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -21,7 +21,7 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points from frappe.model.base_document import get_controller from frappe.social.doctype.post.post import frequently_visited_links -from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings +from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo def get_bootinfo(): """build and return boot info""" @@ -39,7 +39,7 @@ def get_bootinfo(): bootinfo.server_date = frappe.utils.nowdate() if frappe.session['user'] != 'Guest': - bootinfo.user_info = get_fullnames() + bootinfo.user_info = get_user_info() bootinfo.sid = frappe.session['sid'] bootinfo.modules = {} @@ -48,6 +48,7 @@ def get_bootinfo(): bootinfo.letter_heads = get_letter_heads() bootinfo.active_domains = frappe.get_active_domains() bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")] + add_layouts(bootinfo) bootinfo.module_app = frappe.local.module_app bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] @@ -61,6 +62,7 @@ def get_bootinfo(): doclist.extend(get_meta_bundle("Page")) bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() + bootinfo.notification_settings = get_notification_settings() # ipinfo if frappe.session.data.get('ipinfo'): @@ -88,6 +90,8 @@ def get_bootinfo(): bootinfo.frequently_visited_links = frequently_visited_links() bootinfo.link_preview_doctypes = get_link_preview_doctypes() bootinfo.additional_filters_config = get_additional_filters_from_hooks() + bootinfo.desk_settings = get_desk_settings() + bootinfo.app_logo_url = get_app_logo() return bootinfo @@ -106,11 +110,9 @@ def load_conf_settings(bootinfo): if key in conf: bootinfo[key] = conf.get(key) def load_desktop_data(bootinfo): - from frappe.config import get_modules_from_all_apps_for_user from frappe.desk.desktop import get_desk_sidebar_items - bootinfo.allowed_modules = get_modules_from_all_apps_for_user() - bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False) - bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map() + bootinfo.allowed_workspaces = get_desk_sidebar_items() + bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") def get_allowed_pages(cache=False): @@ -222,19 +224,18 @@ def load_translations(bootinfo): bootinfo["__messages"] = messages -def get_fullnames(): - """map of user fullnames""" - ret = frappe.db.sql("""select `name`, full_name as fullname, - user_image as image, gender, email, username, bio, location, interest, banner_image, allowed_in_mentions - from tabUser where enabled=1 and user_type!='Website User'""", as_dict=1) +def get_user_info(): + user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', + 'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'], + filters=dict(enabled=1)) - d = {} - for r in ret: - # if not r.image: - # r.image = get_gravatar(r.name) - d[r.name] = r + user_info_map = {d.name: d for d in user_info} - return d + admin_data = user_info_map.get('Administrator') + if admin_data: + user_info_map[admin_data.email] = admin_data + + return user_info_map def get_user(bootinfo): """get user info""" @@ -251,13 +252,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('workspace') - - bootinfo['home_page'] = page.name - docs.append(page) + bootinfo['home_page'] = 'Workspaces' def add_timezone_info(bootinfo): system = bootinfo.sysdefaults.get("time_zone") @@ -273,7 +273,7 @@ def load_print(bootinfo, doclist): def load_print_css(bootinfo, print_settings): import frappe.www.printview - bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Modern", for_legacy=True) + bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True) def get_unseen_notes(): return frappe.db.sql('''select `name`, title, content, notify_on_every_login from `tabNote` where notify_on_login=1 @@ -308,3 +308,24 @@ def get_additional_filters_from_hooks(): filter_config.update(frappe.get_attr(hook)()) return filter_config + +def add_layouts(bootinfo): + # add routes for readable doctypes + bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type']) + +def get_desk_settings(): + role_list = frappe.get_all('Role', fields=['*'], filters=dict( + name=['in', frappe.get_roles()] + )) + desk_settings = {} + + from frappe.core.doctype.role.role import desk_properties + + for role in role_list: + for key in desk_properties: + desk_settings[key] = desk_settings.get(key) or role.get(key) + + return desk_settings + +def get_notification_settings(): + return frappe.get_cached_doc('Notification Settings', frappe.session.user) diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index ed5c7b64ad..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', @@ -67,10 +67,6 @@ def clear_defaults_cache(user=None): elif frappe.flags.in_install!="frappe": frappe.cache().delete_key("defaults") -def clear_document_cache(): - frappe.local.document_cache = {} - frappe.cache().delete_key("document_cache") - def clear_doctype_cache(doctype=None): clear_controller_cache(doctype) cache = frappe.cache() @@ -78,9 +74,11 @@ def clear_doctype_cache(doctype=None): if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): del frappe.local.meta_cache[doctype] - for key in ('is_table', 'doctype_modules'): + for key in ('is_table', 'doctype_modules', 'document_cache'): cache.delete_value(key) + frappe.local.document_cache = {} + def clear_single(dt): for name in doctype_cache_keys: cache.hdel(name, dt) @@ -102,15 +100,12 @@ def clear_doctype_cache(doctype=None): for name in doctype_cache_keys: cache.delete_value(name) - # Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured - clear_document_cache() - 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) 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/config/__init__.py b/frappe/config/__init__.py index b62a3bc258..cc9d0e6c67 100644 --- a/frappe/config/__init__.py +++ b/frappe/config/__init__.py @@ -108,4 +108,4 @@ def is_domain(module): return module.get("category") == "Domains" def is_module(module): - return module.get("type") == "module" + return module.get("type") == "module" \ No newline at end of file diff --git a/frappe/config/automation.py b/frappe/config/automation.py deleted file mode 100644 index 08de969729..0000000000 --- a/frappe/config/automation.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - data = [ - { - "label": _("Automation"), - "icon": "fa fa-random", - "items": [ - { - "type": "doctype", - "name": "Assignment Rule", - "description": _("Set up rules for user assignments.") - }, - { - "type": "doctype", - "name": "Milestone", - "description": _("Tracks milestones on the lifecycle of a document if it undergoes multiple stages.") - }, - { - "type": "doctype", - "name": "Auto Repeat", - "description": _("Automatically generates recurring documents.") - }, - ] - }, - { - "label": _("Event Streaming"), - "icon": "fa fa-random", - "items": [ - { - "type": "doctype", - "name": "Event Producer", - "description": _("The site you want to subscribe to for consuming events.") - }, - { - "type": "doctype", - "name": "Event Consumer", - "description": _("The site which is consuming your events.") - }, - { - "type": "doctype", - "name": "Event Update Log", - "description": _("Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.") - }, - { - "type": "doctype", - "name": "Event Sync Log", - "description": _("Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.") - }, - { - "type": "doctype", - "name": "Document Type Mapping", - "description": _("The mapping configuration between two doctypes.") - } - ] - } - ] - return data diff --git a/frappe/config/core.py b/frappe/config/core.py deleted file mode 100644 index bdf39dfb3f..0000000000 --- a/frappe/config/core.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return [ - { - "label": _("Documents"), - "items": [ - { - "type": "doctype", - "name": "DocType", - "description": _("Models (building blocks) of the Application"), - }, - { - "type": "doctype", - "name": "Module Def", - "description": _("Groups of DocTypes"), - }, - { - "type": "doctype", - "name": "Page", - "description": _("Pages in Desk (place holders)"), - }, - { - "type": "doctype", - "name": "Report", - "description": _("Script or Query reports"), - }, - { - "type": "doctype", - "name": "Print Format", - "description": _("Customized Formats for Printing, Email"), - }, - { - "type": "doctype", - "name": "Custom Script", - "description": _("Client side script extensions in Javascript"), - } - ] - }, - { - "label": _("Logs"), - "items": [ - { - "type": "doctype", - "name": "Error Log", - "description": _("Errors in Background Events"), - }, - { - "type": "doctype", - "name": "Email Queue", - "description": _("Background Email Queue"), - }, - { - "type": "page", - "label": _("Background Jobs"), - "name": "background_jobs", - }, - { - "type": "doctype", - "name": "Error Snapshot", - "description": _("A log of request errors"), - }, - ] - } - ] diff --git a/frappe/config/customization.py b/frappe/config/customization.py deleted file mode 100644 index 95fa5d355c..0000000000 --- a/frappe/config/customization.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return [ - { - "label": _("Form Customization"), - "icon": "fa fa-glass", - "items": [ - { - "type": "doctype", - "name": "Customize Form", - "description": _("Change field properties (hide, readonly, permission etc.)") - }, - { - "type": "doctype", - "name": "Custom Field", - "description": _("Add fields to forms.") - }, - { - "type": "doctype", - "name": "Custom Script", - "description": _("Add custom javascript to forms.") - }, - { - "type": "doctype", - "name": "DocType", - "description": _("Add custom forms.") - }, - ] - }, - { - "label": _("Dashboards"), - "items": [ - { - "type": "doctype", - "name": "Dashboard", - }, - { - "type": "doctype", - "name": "Dashboard Chart", - }, - { - "type": "doctype", - "name": "Dashboard Chart Source", - }, - ] - }, - { - "label": _("Other"), - "items": [ - { - "type": "doctype", - "label": _("Custom Translations"), - "name": "Translation", - "description": _("Add your own translations") - } - ] - } - ] diff --git a/frappe/config/desk.py b/frappe/config/desk.py deleted file mode 100644 index 40db97ef8c..0000000000 --- a/frappe/config/desk.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return [ - { - "label": _("Tools"), - "icon": "octicon octicon-briefcase", - "items": [ - { - "type": "doctype", - "name": "ToDo", - "label": _("To Do"), - "description": _("Documents assigned to you and by you."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Event", - "label": _("Calendar"), - "link": "List/Event/Calendar", - "description": _("Event and other calendars."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Note", - "description": _("Private and public Notes."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "File", - "label": _("Files"), - }, - { - "type": "page", - "label": _("Chat"), - "name": "chat", - "description": _("Chat messages and other notifications."), - "data_doctype": "Communication" - }, - { - "type": "page", - "label": _("Activity"), - "name": "activity", - "description": _("Activity log of all users."), - }, - ] - }, - { - 'label': _('Email'), - 'items': [ - { - "type": "doctype", - "name": "Newsletter", - "description": _("Newsletters to contacts, leads."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Email Group", - "description": _("Email Group List"), - }, - ] - } - ] diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py deleted file mode 100644 index 568cc76afd..0000000000 --- a/frappe/config/desktop.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe import _ - -def get_data(): - return [ - # Administration - { - "module_name": "Desk", - "category": "Administration", - "label": _("Tools"), - "color": "#FFF5A7", - "reverse": 1, - "icon": "octicon octicon-calendar", - "type": "module", - "description": "Todos, notes, calendar and newsletter." - }, - { - "module_name": "Settings", - "category": "Administration", - "label": _("Settings"), - "color": "#bdc3c7", - "reverse": 1, - "icon": "octicon octicon-settings", - "type": "module", - "description": "Data import, printing, email and workflows." - }, - { - "module_name": "Automation", - "category": "Administration", - "label": _("Automation"), - "color": "#bdc3c7", - "reverse": 1, - "icon": "octicon octicon-gist", - "type": "module", - "description": "Auto Repeat, Assignment Rule, Milestone Tracking and Event Streaming." - }, - { - "module_name": "Users and Permissions", - "category": "Administration", - "label": _("Users and Permissions"), - "color": "#bdc3c7", - "reverse": 1, - "icon": "octicon octicon-settings", - "type": "module", - "description": "Setup roles and permissions for users on documents." - }, - { - "module_name": "Customization", - "category": "Administration", - "label": _("Customization"), - "color": "#bdc3c7", - "reverse": 1, - "icon": "octicon octicon-settings", - "type": "module", - "description": "Customize forms, custom fields, scripts and translations." - }, - { - "module_name": "Integrations", - "category": "Administration", - "label": _("Integrations"), - "color": "#16a085", - "icon": "octicon octicon-globe", - "type": "module", - "description": "DropBox, Woocomerce, AWS, Shopify and GoCardless." - }, - { - "module_name": 'Contacts', - "category": "Administration", - "label": _("Contacts"), - "type": 'module', - "icon": "octicon octicon-book", - "color": '#ffaedb', - "description": "People Contacts and Address Book." - }, - { - "module_name": "Core", - "category": "Administration", - "_label": _("Developer"), - "label": "Developer", - "color": "#589494", - "icon": "octicon octicon-circuit-board", - "type": "module", - "system_manager": 1, - "condition": getattr(frappe.local.conf, 'developer_mode', 0), - "description": "Doctypes, dev tools and logs." - }, - - # Places - { - "module_name": "Website", - "category": "Places", - "label": _("Website"), - "_label": _("Website"), - "color": "#16a085", - "icon": "octicon octicon-globe", - "type": "module", - "description": "Webpages, webforms, blogs and website theme." - }, - { - "module_name": 'Social', - "category": "Places", - "label": _('Social'), - "icon": "octicon octicon-heart", - "type": 'link', - "link": '#social/home', - "color": '#FF4136', - 'standard': 1, - 'idx': 15, - "description": "Build your profile and share posts with other users." - }, - { - "module_name": 'Leaderboard', - "category": "Places", - "label": _('Leaderboard'), - "icon": "fa fa-trophy", - "type": 'link', - "link": '#leaderboard/User', - "color": '#FF4136', - 'standard': 1, - }, - { - "module_name": 'dashboard', - "category": "Places", - "label": _('Dashboard'), - "icon": "octicon octicon-graph", - "type": "link", - "link": "#dashboard", - "color": '#FF4136', - 'standard': 1, - 'idx': 10 - }, - ] diff --git a/frappe/config/docs.py b/frappe/config/docs.py deleted file mode 100644 index eafaeeb19e..0000000000 --- a/frappe/config/docs.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - -source_link = "https://github.com/frappe/frappe_io" -docs_base_url = "/docs" diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py deleted file mode 100644 index 672c0c4acc..0000000000 --- a/frappe/config/integrations.py +++ /dev/null @@ -1,127 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return [ - { - "label": _("Payments"), - "icon": "fa fa-star", - "items": [ - { - "type": "doctype", - "name": "Braintree Settings", - "description": _("Braintree payment gateway settings"), - }, - { - "type": "doctype", - "name": "PayPal Settings", - "description": _("PayPal payment gateway settings"), - }, - { - "type": "doctype", - "name": "Razorpay Settings", - "description": _("Razorpay Payment gateway settings"), - }, - { - "type": "doctype", - "name": "Stripe Settings", - "description": _("Stripe payment gateway settings"), - }, - { - "type": "doctype", - "name": "Paytm Settings", - "description": _("Paytm payment gateway settings"), - }, - ] - }, - { - "label": _("Backup"), - "items": [ - { - "type": "doctype", - "name": "Dropbox Settings", - "description": _("Dropbox backup settings"), - }, - { - "type": "doctype", - "name": "S3 Backup Settings", - "description": _("S3 Backup Settings"), - }, - { - "type": "doctype", - "name": "Google Drive", - "description": _("Google Drive Backup."), - } - ] - }, - { - "label": _("Authentication"), - "items": [ - { - "type": "doctype", - "name": "Social Login Key", - "description": _("Enter keys to enable login via Facebook, Google, GitHub."), - }, - { - "type": "doctype", - "name": "LDAP Settings", - "description": _("Ldap settings"), - }, - { - "type": "doctype", - "name": "OAuth Client", - "description": _("Register OAuth Client App"), - }, - { - "type": "doctype", - "name": "OAuth Provider Settings", - "description": _("Settings for OAuth Provider"), - }, - { - "type": "doctype", - "name": "Connected App", - "description": _("Connect to any OAuth Provider"), - }, - ] - }, - { - "label": _("Webhook"), - "items": [ - { - "type": "doctype", - "name": "Webhook", - "description": _("Webhooks calling API requests into web apps"), - }, - { - "type": "doctype", - "name": "Slack Webhook URL", - "description": _("Slack Webhooks for internal integration"), - }, - ] - }, - { - "label": _("Google Services"), - "items": [ - { - "type": "doctype", - "name": "Google Settings", - "description": _("Google API Settings."), - }, - { - "type": "doctype", - "name": "Google Contacts", - "description": _("Google Contacts Integration."), - }, - { - "type": "doctype", - "name": "Google Calendar", - "description": _("Google Calendar Integration."), - }, - { - "type": "doctype", - "name": "Google Drive", - "description": _("Google Drive Integration."), - } - ] - } - ] diff --git a/frappe/config/settings.py b/frappe/config/settings.py deleted file mode 100644 index 0112c7ccff..0000000000 --- a/frappe/config/settings.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.desk.moduleview import add_setup_section - -def get_data(): - data = [ - { - "label": _("Core"), - "icon": "fa fa-wrench", - "items": [ - { - "type": "doctype", - "name": "System Settings", - "label": _("System Settings"), - "description": _("Language, Date and Time settings"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Global Defaults", - "label": _("Global Defaults"), - "description": _("Company, Fiscal Year and Currency defaults"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Log Settings", - "description": _("Log cleanup and notification configuration") - }, - { - "type": "doctype", - "name": "Error Log", - "description": _("Log of error on automated events (scheduler).") - }, - { - "type": "doctype", - "name": "Error Snapshot", - "description": _("Log of error during requests.") - }, - { - "type": "doctype", - "name": "Domain Settings", - "label": _("Domain Settings"), - "description": _("Enable / Disable Domains"), - "hide_count": True - }, - ] - }, - { - "label": _("Data"), - "icon": "fa fa-th", - "items": [ - { - "type": "doctype", - "name": "Data Import", - "label": _("Import Data"), - "icon": "octicon octicon-cloud-upload", - "description": _("Import Data from CSV / Excel files.") - }, - { - "type": "doctype", - "name": "Data Export", - "label": _("Export Data"), - "icon": "octicon octicon-cloud-upload", - "description": _("Export Data in CSV / Excel format.") - }, - { - "type": "doctype", - "name": "Naming Series", - "description": _("Set numbering series for transactions."), - "hide_count": True - }, - { - "type": "doctype", - "name": "Rename Tool", - "label": _("Bulk Rename"), - "description": _("Rename many items by uploading a .csv file."), - "hide_count": True - }, - { - "type": "doctype", - "name": "Bulk Update", - "label": _("Bulk Update"), - "description": _("Update many values at one time."), - "hide_count": True - }, - { - "type": "page", - "name": "backups", - "label": _("Download Backups"), - "description": _("List of backups available for download"), - "icon": "fa fa-download" - }, - { - "type": "doctype", - "name": "Deleted Document", - "label": _("Deleted Documents"), - "description": _("Restore or permanently delete a document.") - }, - ] - }, - { - "label": _("Email / Notifications"), - "icon": "fa fa-envelope", - "items": [ - { - "type": "doctype", - "name": "Email Account", - "description": _("Add / Manage Email Accounts.") - }, - { - "type": "doctype", - "name": "Email Domain", - "description": _("Add / Manage Email Domains.") - }, - { - "type": "doctype", - "name": "Notification", - "description": _("Setup Notifications based on various criteria.") - }, - { - "type": "doctype", - "name": "Email Template", - "description": _("Email Templates for common queries.") - }, - { - "type": "doctype", - "name": "Auto Email Report", - "description": _("Setup Reports to be emailed at regular intervals"), - }, - { - "type": "doctype", - "name": "Newsletter", - "description": _("Create and manage newsletter") - }, - { - "type": "doctype", - "route": "Form/Notification Settings/{}".format(frappe.session.user), - "name": "Notification Settings", - "description": _("Configure notifications for mentions, assignments, energy points and more.") - } - ] - }, - { - "label": _("Printing"), - "icon": "fa fa-print", - "items": [ - { - "type": "page", - "label": _("Print Format Builder"), - "name": "print-format-builder", - "description": _("Drag and Drop tool to build and customize Print Formats.") - }, - { - "type": "doctype", - "name": "Print Settings", - "description": _("Set default format, page size, print style etc.") - }, - { - "type": "doctype", - "name": "Print Format", - "description": _("Customized HTML Templates for printing transactions.") - }, - { - "type": "doctype", - "name": "Print Style", - "description": _("Stylesheets for Print Formats") - }, - ] - }, - { - "label": _("Workflow"), - "icon": "fa fa-random", - "items": [ - { - "type": "doctype", - "name": "Workflow", - "description": _("Define workflows for forms.") - }, - { - "type": "doctype", - "name": "Workflow State", - "description": _("States for workflow (e.g. Draft, Approved, Cancelled).") - }, - { - "type": "doctype", - "name": "Workflow Action", - "description": _("Actions for workflow (e.g. Approve, Cancel).") - }, - ] - } - ] - add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe") - return data diff --git a/frappe/config/tools.py b/frappe/config/tools.py deleted file mode 100644 index 3ab2a59222..0000000000 --- a/frappe/config/tools.py +++ /dev/null @@ -1,2 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ \ No newline at end of file diff --git a/frappe/config/users_and_permissions.py b/frappe/config/users_and_permissions.py deleted file mode 100644 index d50235e4a3..0000000000 --- a/frappe/config/users_and_permissions.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return [ - { - "label": _("Users"), - "icon": "fa fa-group", - "items": [ - { - "type": "doctype", - "name": "User", - "description": _("System and Website Users") - }, - { - "type": "doctype", - "name": "Role", - "description": _("User Roles") - }, - { - "type": "doctype", - "name": "Role Profile", - "description": _("Role Profile") - } - ] - }, - { - "label": _("Permissions"), - "icon": "fa fa-lock", - "items": [ - { - "type": "page", - "name": "permission-manager", - "label": _("Role Permissions Manager"), - "icon": "fa fa-lock", - "description": _("Set Permissions on Document Types and Roles") - }, - { - "type": "doctype", - "name": "User Permission", - "label": _("User Permissions"), - "icon": "fa fa-lock", - "description": _("Restrict user for specific document") - }, - { - "type": "doctype", - "name": "Role Permission for Page and Report", - "description": _("Set custom roles for page and report") - }, - { - "type": "report", - "is_query_report": True, - "doctype": "User", - "icon": "fa fa-eye-open", - "name": "Permitted Documents For User", - "description": _("Check which Documents are readable by a User") - }, - { - "type": "report", - "doctype": "DocShare", - "icon": "fa fa-share", - "name": "Document Share Report", - "description": _("Report of all document shares") - } - ] - }, - { - "label": _("Logs"), - "icon": "fa fa-group", - "items": [ - { - "type": "doctype", - "name": "Activity Log", - "label": _("Activity Log"), - "description": _("Activity Log by ") - }, - { - "type": "doctype", - "name": "Access Log", - "label": _("Access Log"), - "description": _("View Log of all print, download and export events") - } - ] - } - ] \ No newline at end of file diff --git a/frappe/config/website.py b/frappe/config/website.py deleted file mode 100644 index e7eb218889..0000000000 --- a/frappe/config/website.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return [ - { - "label": _("Web Site"), - "icon": "fa fa-star", - "items": [ - { - "type": "doctype", - "name": "Web Page", - "description": _("Content web page."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Web Form", - "description": _("User editable form on Website."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Website Sidebar", - }, - { - "type": "doctype", - "name": "Website Slideshow", - "description": _("Embed image slideshows in website pages."), - }, - { - "type": "doctype", - "name": "Website Route Meta", - "description": _("Add meta tags to your web pages"), - }, - ] - }, - { - "label": _("Blog"), - "items": [ - { - "type": "doctype", - "name": "Blog Post", - "description": _("Single Post (article)."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Blogger", - "description": _("A user who posts blogs."), - }, - { - "type": "doctype", - "name": "Blog Category", - "description": _("Categorize blog posts."), - }, - ] - }, - { - "label": _("Setup"), - "icon": "fa fa-cog", - "items": [ - { - "type": "doctype", - "name": "Website Settings", - "description": _("Setup of top navigation bar, footer and logo."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Website Theme", - "description": _("List of themes for Website."), - "onboard": 1, - }, - { - "type": "doctype", - "name": "Website Script", - "description": _("Javascript to append to the head section of the page."), - }, - { - "type": "doctype", - "name": "About Us Settings", - "description": _("Settings for About Us Page."), - }, - { - "type": "doctype", - "name": "Contact Us Settings", - "description": _("Settings for Contact Us Page."), - }, - ] - }, - { - "label": _("Portal"), - "items": [ - { - "type": "doctype", - "name": "Portal Settings", - "label": _("Portal Settings"), - "onboard": 1, - } - ] - }, - { - "label": _("Knowledge Base"), - "items": [ - { - "type": "doctype", - "name": "Help Category", - }, - { - "type": "doctype", - "name": "Help Article", - }, - ] - }, - - ] diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 2e2fb6df67..696cd61d6c 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -34,8 +34,8 @@ "email_ids", "phone_nos", "contact_details", - "is_primary_contact", "links", + "is_primary_contact", "more_info", "department", "unsubscribed" @@ -248,8 +248,9 @@ "icon": "fa fa-user", "idx": 1, "image_field": "image", + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-04-06 18:25:28.223693", + "modified": "2020-08-27 14:12:09.906719", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", 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/desk_page/settings/settings.json b/frappe/core/desk_page/settings/settings.json deleted file mode 100644 index 642a4fdadd..0000000000 --- a/frappe/core/desk_page/settings/settings.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "cards": [ - { - "hidden": 0, - "label": "Data", - "links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Email / Notifications", - "links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Website", - "links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Core", - "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Printing", - "links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Workflow", - "links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]" - } - ], - "category": "Modules", - "charts": [], - "creation": "2020-03-02 15:09:40.527211", - "developer_mode_only": 0, - "disable_user_customization": 1, - "docstatus": 0, - "doctype": "Desk Page", - "extends_another_page": 0, - "hide_custom": 0, - "idx": 0, - "is_standard": 1, - "label": "Settings", - "modified": "2020-07-14 10:09:09.520557", - "modified_by": "Administrator", - "module": "Core", - "name": "Settings", - "owner": "Administrator", - "pin_to_bottom": 1, - "pin_to_top": 0, - "shortcuts": [ - { - "icon": "octicon octicon-settings", - "label": "System Settings", - "link_to": "System Settings", - "type": "DocType" - }, - { - "icon": "fa fa-print", - "label": "Print Settings", - "link_to": "Print Settings", - "type": "DocType" - }, - { - "icon": "fa fa-globe", - "label": "Website Settings", - "link_to": "Website Settings", - "type": "DocType" - } - ], - "shortcuts_label": "Settings" -} \ No newline at end of file diff --git a/frappe/core/desk_page/users/users.json b/frappe/core/desk_page/users/users.json deleted file mode 100644 index f1f43a4ef0..0000000000 --- a/frappe/core/desk_page/users/users.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "cards": [ - { - "hidden": 0, - "label": "Users", - "links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Logs", - "links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Permissions", - "links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]" - } - ], - "category": "Administration", - "charts": [], - "creation": "2020-03-02 15:12:16.754449", - "developer_mode_only": 0, - "disable_user_customization": 0, - "docstatus": 0, - "doctype": "Desk Page", - "extends_another_page": 0, - "idx": 0, - "is_standard": 1, - "label": "Users", - "modified": "2020-04-26 22:36:14.311554", - "modified_by": "Administrator", - "module": "Core", - "name": "Users", - "owner": "Administrator", - "pin_to_bottom": 0, - "pin_to_top": 0, - "shortcuts": [ - { - "label": "User", - "link_to": "User", - "type": "DocType" - }, - { - "label": "Role", - "link_to": "Role", - "type": "DocType" - }, - { - "label": "Permission Manager", - "link_to": "permission-manager", - "type": "Page" - }, - { - "label": "User Profile", - "link_to": "user-profile", - "type": "Page" - } - ] -} \ No newline at end of file diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 04ecc83b38..e4fd181733 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -18,10 +18,7 @@ from frappe.exceptions import ImplicitCommitError class Comment(Document): def after_insert(self): self.notify_mentions() - - frappe.publish_realtime('new_communication', self.as_dict(), - doctype=self.reference_doctype, docname=self.reference_name, - after_commit=True) + self.notify_change('add') def validate(self): if not self.comment_email: @@ -30,12 +27,30 @@ 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() - frappe.publish_realtime('delete_communication', self.as_dict(), - doctype= self.reference_doctype, docname = self.reference_name, - after_commit=True) + self.notify_change('delete') + + def notify_change(self, action): + key_map = { + 'Like': 'like_logs', + 'Assigned': 'assignment_logs', + 'Assignment Completed': 'assignment_logs', + 'Comment': 'comments', + 'Attachment': 'attachment_logs', + 'Attachment Removed': 'attachment_logs', + } + key = key_map.get(self.comment_type) + if not key: return + + frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { + 'doc': self.as_dict(), + 'key': key, + 'action': action + }, after_commit=True) def remove_comment_from_cache(self): _comments = get_comments_from_parent(self) diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index cdbdf97852..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('Document not Relinked') + function() { + frappe.show_alert({ + message: __('Document not Relinked'), 'indicator': 'info' + }); } ); } diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index d893e80617..5ebf714645 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -99,10 +99,7 @@ class Communication(Document): frappe.db.set_value("Communication", self.reference_name, "status", "Replied") if self.communication_type == "Communication": - # send new comment to listening clients - frappe.publish_realtime('new_communication', self.as_dict(), - doctype=self.reference_doctype, docname=self.reference_name, - after_commit=True) + self.notify_change('add') elif self.communication_type in ("Chat", "Notification", "Bot"): if self.reference_name == frappe.session.user: @@ -125,10 +122,14 @@ class Communication(Document): def on_trash(self): if self.communication_type == "Communication": - # send delete comment to listening clients - frappe.publish_realtime('delete_communication', self.as_dict(), - doctype= self.reference_doctype, docname = self.reference_name, - after_commit=True) + self.notify_change('delete') + + def notify_change(self, action): + frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { + 'doc': self.as_dict(), + 'key': 'communications', + 'action': action + }, after_commit=True) def set_status(self): if not self.is_new(): @@ -244,9 +245,7 @@ class Communication(Document): if delivery_status: self.db_set('delivery_status', delivery_status) - - frappe.publish_realtime('update_communication', self.as_dict(), - doctype=self.reference_doctype, docname=self.reference_name, after_commit=True) + self.notify_change('update') # for list views and forms self.notify_update() diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy.js b/frappe/core/doctype/data_import_legacy/data_import_legacy.js index 9a301af76e..8e4f397171 100644 --- a/frappe/core/doctype/data_import_legacy/data_import_legacy.js +++ b/frappe/core/doctype/data_import_legacy/data_import_legacy.js @@ -32,7 +32,7 @@ frappe.ui.form.on('Data Import Legacy', { frm.reload_doc(); } if (data.progress) { - let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar"); + let progress_bar = $(frm.dashboard.progress_area.body).find(".progress-bar"); if (progress_bar) { $(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped"); $(progress_bar).css("width", data.progress + "%"); diff --git a/frappe/core/doctype/deleted_document/deleted_document_list.js b/frappe/core/doctype/deleted_document/deleted_document_list.js index f5e1147dfb..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 `
  • ${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/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index b3469abf29..3e2a423b06 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -24,11 +24,11 @@ frappe.ui.form.on('DocType', { if (!frm.is_new() && !frm.doc.istable) { if (frm.doc.issingle) { frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => { - frappe.set_route('Form', frm.doc.name); + window.open(`/app/${frappe.router.slug(frm.doc.name)}`); }); } else { frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => { - frappe.set_route('List', frm.doc.name, 'List'); + window.open(`/app/${frappe.router.slug(frm.doc.name)}`); }); } } diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 215ef8cd62..1533829b3c 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -132,7 +132,7 @@ "label": "Editable Grid" }, { - "default": "1", + "default": "0", "depends_on": "eval:!doc.istable && !doc.issingle", "description": "Open a dialog with mandatory fields to create a new record quickly", "fieldname": "quick_entry", @@ -427,7 +427,7 @@ "label": "Allow Guest to View" }, { - "depends_on": "has_web_view", + "depends_on": "eval:!doc.istable", "fieldname": "route", "fieldtype": "Data", "label": "Route" @@ -555,7 +555,7 @@ }, { "group": "Customization", - "link_doctype": "Custom Script", + "link_doctype": "Client Script", "link_fieldname": "dt" }, { @@ -609,7 +609,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2020-09-24 13:13:58.227153", + "modified": "2021-02-04 15:10:09.227205", "modified_by": "Administrator", "module": "Core", "name": "DocType", @@ -637,6 +637,7 @@ "write": 1 } ], + "route": "doctype", "search_fields": "module", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 80a576230c..cbcfa350f5 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -26,7 +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 @@ -63,6 +63,37 @@ class DocType(Document): self.validate_name() + self.set_defaults_for_single_and_table() + self.scrub_field_names() + self.set_default_in_list_view() + self.set_default_translatable() + self.validate_series() + self.validate_document_type() + validate_fields(self) + + if not self.istable: + validate_permissions(self) + + self.make_amendable() + self.make_repeatable() + self.validate_nestedset() + self.validate_website() + validate_links_table_fieldnames(self) + + if not self.is_new(): + self.before_update = frappe.get_doc('DocType', self.name) + self.setup_fields_to_fetch() + + check_email_append_to(self) + + if self.default_print_format and not self.custom: + frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) + + def after_insert(self): + # clear user cache so that on the next reload this doctype is included in boot + clear_user_cache(frappe.session.user) + + def set_defaults_for_single_and_table(self): if self.issingle: self.allow_import = 0 self.is_submittable = 0 @@ -72,44 +103,6 @@ class DocType(Document): self.allow_import = 0 self.permissions = [] - self.scrub_field_names() - self.set_default_in_list_view() - self.set_default_translatable() - self.validate_series() - self.validate_document_type() - validate_fields(self) - - if self.istable: - # no permission records for child table - self.permissions = [] - else: - validate_permissions(self) - - self.make_amendable() - self.make_repeatable() - self.validate_nestedset() - self.validate_website() - self.validate_links_table_fieldnames() - - if not self.is_new(): - self.before_update = frappe.get_doc('DocType', self.name) - - if not self.is_new(): - self.setup_fields_to_fetch() - - check_email_append_to(self) - - if self.default_print_format and not self.custom: - frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) - - if frappe.conf.get('developer_mode'): - self.owner = 'Administrator' - self.modified_by = 'Administrator' - - def after_insert(self): - # clear user cache so that on the next reload this doctype is included in boot - clear_user_cache(frappe.session.user) - def set_default_in_list_view(self): '''Set default in-list-view for first 4 mandatory fields''' if not [d.fieldname for d in self.fields if d.in_list_view]: @@ -134,6 +127,10 @@ class DocType(Document): if not frappe.conf.get("developer_mode") and not self.custom: frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError) + if frappe.conf.get('developer_mode'): + self.owner = 'Administrator' + self.modified_by = 'Administrator' + def setup_fields_to_fetch(self): '''Setup query to update values for newly set fetch values''' try: @@ -192,6 +189,9 @@ class DocType(Document): def validate_website(self): """Ensure that website generator has field 'route'""" + if self.route: + self.route = self.route.strip('/') + if self.has_web_view: # route field must be present if not 'route' in [d.fieldname for d in self.fields]: @@ -278,7 +278,6 @@ class DocType(Document): def on_update(self): """Update database schema, make controller templates if `custom` is not set and clear cache.""" - self.delete_duplicate_custom_fields() try: frappe.db.updatedb(self.name, Meta(self)) except Exception as e: @@ -323,18 +322,6 @@ class DocType(Document): clear_linked_doctype_cache() - def delete_duplicate_custom_fields(self): - if not (frappe.db.table_exists(self.name) and frappe.db.table_exists("Custom Field")): - return - - fields = [d.fieldname for d in self.fields if d.fieldtype in data_fieldtypes] - if fields: - frappe.db.sql('''delete from - `tabCustom Field` - where - dt = {0} and fieldname in ({1}) - '''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True) - def sync_global_search(self): '''If global search settings are changed, rebuild search properties for this table''' global_search_fields_before_update = [d.fieldname for d in @@ -669,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 @@ -677,24 +664,24 @@ 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) - def validate_links_table_fieldnames(self): - """Validate fieldnames in Links table""" - if frappe.flags.in_patch: return - if frappe.flags.in_fixtures: return - if not self.links: return - - for index, link in enumerate(self.links): - meta = frappe.get_meta(link.link_doctype) - if not meta.get_field(link.link_fieldname): - message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)) - frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname")) + validate_route_conflict(self.doctype, self.name) +def validate_links_table_fieldnames(meta): + """Validate fieldnames in Links table""" + if frappe.flags.in_patch: return + if frappe.flags.in_fixtures: return + if not meta.links: return + for index, link in enumerate(meta.links): + link_meta = frappe.get_meta(link.link_doctype) + if not link_meta.get_field(link.link_fieldname): + message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)) + frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname")) def validate_fields_for_doctype(doctype): - doc = frappe.get_doc("DocType", doctype) - doc.delete_duplicate_custom_fields() - validate_fields(frappe.get_meta(doctype, cached=False)) + meta = frappe.get_meta(doctype, cached=False) + validate_links_table_fieldnames(meta) + validate_fields(meta) # this is separate because it is also called via custom field def validate_fields(meta): diff --git a/frappe/core/doctype/doctype/patches/set_route.py b/frappe/core/doctype/doctype/patches/set_route.py new file mode 100644 index 0000000000..c052a51f38 --- /dev/null +++ b/frappe/core/doctype/doctype/patches/set_route.py @@ -0,0 +1,7 @@ +import frappe +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', 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 10169073e5..ec88a2d14c 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -5,12 +5,17 @@ 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') - class TestDocType(unittest.TestCase): def test_validate_name(self): self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) @@ -459,7 +464,7 @@ class TestDocType(unittest.TestCase): 'link_doctype': "User", 'link_fieldname': "first_name" }) - doc.validate_links_table_fieldnames() # no error + validate_links_table_fieldnames(doc) # no error doc.links = [] # reset links table # check invalid doctype @@ -467,7 +472,7 @@ class TestDocType(unittest.TestCase): 'link_doctype': "User2", 'link_fieldname': "first_name" }) - self.assertRaises(frappe.DoesNotExistError, doc.validate_links_table_fieldnames) + self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc) doc.links = [] # reset links table # check invalid fieldname @@ -475,7 +480,7 @@ class TestDocType(unittest.TestCase): 'link_doctype': "User", 'link_fieldname': "a_field_that_does_not_exists" }) - self.assertRaises(InvalidFieldNameError, doc.validate_links_table_fieldnames) + self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) def new_doctype(name, unique=0, depends_on='', fields=None): 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/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index 6d59cdeb29..08e61d3289 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -36,7 +36,7 @@ def has_unseen_error_log(user): def _get_response(show_alert=True): return { 'show_alert': True, - 'message': _("You have unseen {0}").format(' Error Logs ') + 'message': _("You have unseen {0}").format(' Error Logs ') } if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"): diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json index b841eb1f0b..7a8bfd76a7 100644 --- a/frappe/core/doctype/module_def/module_def.json +++ b/frappe/core/doctype/module_def/module_def.json @@ -51,7 +51,7 @@ "link_fieldname": "module" }, { - "link_doctype": "Desk Page", + "link_doctype": "Workspace", "link_fieldname": "module" } ], diff --git a/frappe/core/page/dashboard/__init__.py b/frappe/core/doctype/module_profile/__init__.py similarity index 100% rename from frappe/core/page/dashboard/__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/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py index f7c437bf00..2244bc9e4e 100644 --- a/frappe/core/doctype/navbar_settings/navbar_settings.py +++ b/frappe/core/doctype/navbar_settings/navbar_settings.py @@ -23,9 +23,9 @@ class NavbarSettings(Document): if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)): frappe.throw(_("Please hide the standard navbar items instead of deleting them")) -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def get_app_logo(): - app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo') + app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True) if not app_logo: app_logo = frappe.get_hooks('app_logo_url')[-1] diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 65befcded5..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,6 +34,8 @@ class Page(Document): self.name += '-' + str(cnt) def validate(self): + 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/role/patches/v13_set_default_desk_properties.py b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py new file mode 100644 index 0000000000..375ea02e0e --- /dev/null +++ b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py @@ -0,0 +1,10 @@ +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: + role_doc.set(key, role_doc.desk_access) + role_doc.save() \ No newline at end of file diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 0ad15ba10b..e47dc7194b 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -13,7 +13,19 @@ "column_break_4", "disabled", "desk_access", - "two_factor_auth" + "two_factor_auth", + "navigation_settings_section", + "search_bar", + "notifications", + "chat", + "list_settings_section", + "list_sidebar", + "bulk_actions", + "view_switcher", + "form_settings_section", + "form_sidebar", + "timeline", + "dashboard" ], "fields": [ { @@ -60,12 +72,82 @@ { "fieldname": "column_break_4", "fieldtype": "Column Break" + }, + { + "fieldname": "navigation_settings_section", + "fieldtype": "Section Break", + "label": "Navigation Settings" + }, + { + "default": "1", + "fieldname": "search_bar", + "fieldtype": "Check", + "label": "Search Bar" + }, + { + "default": "1", + "fieldname": "chat", + "fieldtype": "Check", + "label": "Chat" + }, + { + "fieldname": "list_settings_section", + "fieldtype": "Section Break", + "label": "List Settings" + }, + { + "default": "1", + "fieldname": "list_sidebar", + "fieldtype": "Check", + "label": "Sidebar" + }, + { + "default": "1", + "fieldname": "bulk_actions", + "fieldtype": "Check", + "label": "Bulk Actions" + }, + { + "fieldname": "form_settings_section", + "fieldtype": "Section Break", + "label": "Form Settings" + }, + { + "default": "1", + "fieldname": "form_sidebar", + "fieldtype": "Check", + "label": "Sidebar" + }, + { + "default": "1", + "fieldname": "timeline", + "fieldtype": "Check", + "label": "Timeline" + }, + { + "default": "1", + "fieldname": "dashboard", + "fieldtype": "Check", + "label": "Dashboard" + }, + { + "default": "1", + "fieldname": "view_switcher", + "fieldtype": "Check", + "label": "View Switcher" + }, + { + "default": "1", + "fieldname": "notifications", + "fieldtype": "Check", + "label": "Notifications" } ], "icon": "fa fa-bookmark", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-06 15:42:59.036960", + "modified": "2020-12-03 14:08:38.181035", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 1920189f78..7adfeba8d9 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -6,6 +6,9 @@ import frappe from frappe.model.document import Document +desk_properties = ("search_bar", "notifications", "chat", "list_sidebar", + "bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard") + class Role(Document): def before_rename(self, old, new, merge=False): if old in ("Guest", "Administrator", "System Manager", "All"): @@ -16,11 +19,28 @@ class Role(Document): def validate(self): if self.disabled: - if self.name in ("Guest", "Administrator", "System Manager", "All"): - frappe.throw(frappe._("Standard roles cannot be disabled")) - else: - frappe.db.sql("delete from `tabHas Role` where role = %s", self.name) - frappe.clear_cache() + self.disable_role() + else: + self.set_desk_properties() + + def disable_role(self): + if self.name in ("Guest", "Administrator", "System Manager", "All"): + frappe.throw(frappe._("Standard roles cannot be disabled")) + else: + self.remove_roles() + + 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) + + def remove_roles(self): + frappe.db.sql("delete from `tabHas Role` where role = %s", self.name) + frappe.clear_cache() def on_update(self): '''update system user desk access if this has changed in this update''' diff --git a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js index 8a121f31ae..5048d24077 100644 --- a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js +++ b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js @@ -3,32 +3,32 @@ frappe.ui.form.on('Role Permission for Page and Report', { setup: function(frm) { - frm.trigger("set_queries") + frm.trigger("set_queries"); }, refresh: function(frm) { frm.disable_save(); frm.role_area.hide(); - frm.events.add_custom_buttons(frm); + frm.events.setup_buttons(frm); }, - add_custom_buttons: function(frm) { + setup_buttons: function(frm) { frm.clear_custom_buttons(); - if(frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) { + frm.page.clear_actions(); + if (frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) { frm.add_custom_button(__("Reset to defaults"), function() { frm.trigger("reset_roles"); }); - frm.add_custom_button(__("Update"), function() { + frm.page.set_primary_action(__("Update"), () => { frm.trigger("update_report_page_data"); - }).addClass('btn-primary'); + }); } }, onload: function(frm) { - if(!frm.roles_editor) { - frm.role_area = $('
    ') - .appendTo(frm.fields_dict.roles_html.wrapper); + if (!frm.roles_editor) { + frm.role_area = $(frm.fields_dict.roles_html.wrapper); frm.roles_editor = new frappe.RoleEditor(frm.role_area, frm); } }, @@ -54,17 +54,17 @@ frappe.ui.form.on('Role Permission for Page and Report', { }, page: function(frm) { - frm.events.add_custom_buttons(frm); - if(frm.doc.page) { + frm.events.setup_buttons(frm); + if (frm.doc.page) { frm.trigger("set_report_page_data"); } else { frm.trigger("set_role_for"); } }, - report: function(frm){ - frm.events.add_custom_buttons(frm); - if(frm.doc.report) { + report: function(frm) { + frm.events.setup_buttons(frm); + if (frm.doc.report) { frm.trigger("set_report_page_data"); } else { frm.trigger("set_role_for"); diff --git a/frappe/core/doctype/role_profile/role_profile.js b/frappe/core/doctype/role_profile/role_profile.js index d31618cc4a..e43980770a 100644 --- a/frappe/core/doctype/role_profile/role_profile.js +++ b/frappe/core/doctype/role_profile/role_profile.js @@ -3,20 +3,18 @@ frappe.ui.form.on('Role Profile', { refresh: function(frm) { - if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) { - if(!frm.roles_editor) { - var role_area = $('
    ') - .appendTo(frm.fields_dict.roles_html.wrapper); + if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) { + if (!frm.roles_editor) { + const role_area = $(frm.fields_dict.roles_html.wrapper); frm.roles_editor = new frappe.RoleEditor(role_area, frm); - frm.roles_editor.show(); - } else { - frm.roles_editor.show(); } + frm.roles_editor.show(); + } }, validate: function(frm) { - if(frm.roles_editor) { + if (frm.roles_editor) { frm.roles_editor.set_roles_in_table(); } } diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 0d6aa3d7d1..e02d9e5db0 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -3,6 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals +from typing import Dict, List import frappe, json from frappe.model.document import Document @@ -11,12 +12,13 @@ from datetime import datetime from croniter import croniter from frappe.utils.background_jobs import enqueue, get_jobs + class ScheduledJobType(Document): def autoname(self): - self.name = '.'.join(self.method.split('.')[-2:]) + self.name = ".".join(self.method.split(".")[-2:]) def validate(self): - if self.frequency != 'All': + if self.frequency != "All": # force logging for all events other than continuous ones (ALL) self.create_log = 1 @@ -84,7 +86,7 @@ class ScheduledJobType(Document): def log_status(self, status): # log file - frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site)) + frappe.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {frappe.local.site}") self.update_scheduler_log(status) def update_scheduler_log(self, status): @@ -111,28 +113,28 @@ class ScheduledJobType(Document): @frappe.whitelist() -def execute_event(doc): - frappe.only_for('System Manager') +def execute_event(doc: str): + frappe.only_for("System Manager") doc = json.loads(doc) - frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True) + frappe.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True) -def run_scheduled_job(job_type): - '''This is a wrapper function that runs a hooks.scheduler_events method''' +def run_scheduled_job(job_type: str): + """This is a wrapper function that runs a hooks.scheduler_events method""" try: - frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute() except Exception: print(frappe.get_traceback()) -def sync_jobs(hooks=None): +def sync_jobs(hooks: Dict = None): frappe.reload_doc("core", "doctype", "scheduled_job_type") scheduler_events = hooks or frappe.get_hooks("scheduler_events") all_events = insert_events(scheduler_events) clear_events(all_events) -def insert_events(scheduler_events): +def insert_events(scheduler_events: Dict) -> List: cron_jobs, event_jobs = [], [] for event_type in scheduler_events: events = scheduler_events.get(event_type) @@ -144,7 +146,7 @@ def insert_events(scheduler_events): return cron_jobs + event_jobs -def insert_cron_jobs(events): +def insert_cron_jobs(events: Dict) -> List: cron_jobs = [] for cron_format in events: for event in events.get(cron_format): @@ -153,25 +155,29 @@ def insert_cron_jobs(events): return cron_jobs -def insert_event_jobs(events, event_type): +def insert_event_jobs(events: List, event_type: str) -> List: event_jobs = [] for event in events: event_jobs.append(event) - frequency = event_type.replace('_', ' ').title() + frequency = event_type.replace("_", " ").title() insert_single_event(frequency, event) return event_jobs -def insert_single_event(frequency, event, cron_format=None): +def insert_single_event(frequency: str, event: str, cron_format: str = None): cron_expr = {"cron_format": cron_format} if cron_format else {} - doc = frappe.get_doc({ - "doctype": "Scheduled Job Type", - "method": event, - "cron_format": cron_format, - "frequency": frequency - }) + doc = frappe.get_doc( + { + "doctype": "Scheduled Job Type", + "method": event, + "cron_format": cron_format, + "frequency": frequency, + } + ) - if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }): + if not frappe.db.exists( + "Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr} + ): try: doc.insert() except frappe.DuplicateEntryError: @@ -179,7 +185,12 @@ def insert_single_event(frequency, event, cron_format=None): doc.insert() -def clear_events(all_events): - for event in frappe.get_all("Scheduled Job Type", ("name", "method")): - if event.method not in all_events: +def clear_events(all_events: List): + for event in frappe.get_all( + "Scheduled Job Type", fields=["name", "method", "server_script"] + ): + is_server_script = event.server_script + is_defined_in_hooks = event.method in all_events + + if not (is_defined_in_hooks or is_server_script): frappe.delete_doc("Scheduled Job Type", event.name) diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index a317d69166..95a63780f8 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -6,46 +6,11 @@ frappe.ui.form.on('Server Script', { frm.trigger('setup_help'); }, refresh: function(frm) { - if (frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled) { - frm.add_custom_button('Schedule Script', function() { - var d = new frappe.ui.Dialog({ - title: "Schedule Script Execution", - fields: [ - { - fieldname: "event_type", - label: __('Select Event Type'), - fieldtype: "Select", - options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" - }, - ], - primary_action_label: __('Schedule Script'), - primary_action: () => { - d.get_primary_btn().attr('disabled', true); - var data = d.get_values(); - d.hide(); - if(data) { - frm.events.schedule_script(frm, data); - } - - } - }); - - d.show(); - - }); + if (frm.doc.script_type != 'Scheduler Event') { + frm.dashboard.hide(); } }, - schedule_script(frm, data) { - frm.call({ - method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events", - args: { - 'script_name': frm.doc.name, - 'frequency': data.event_type - } - }); - }, - setup_help(frm) { frm.get_field('help_html').html(`

    DocType Event

    diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 9aa7b5afe5..b7e49673f8 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -8,6 +8,7 @@ "field_order": [ "script_type", "reference_doctype", + "event_frequency", "doctype_event", "api_method", "allow_guest", @@ -84,11 +85,24 @@ { "fieldname": "help_html", "fieldtype": "HTML" + }, + { + "depends_on": "eval:doc.script_type == \"Scheduler Event\"", + "fieldname": "event_frequency", + "fieldtype": "Select", + "label": "Event Frequency", + "mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"", + "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" } ], "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-01-03 18:50:14.767595", + "links": [ + { + "link_doctype": "Scheduled Job Type", + "link_fieldname": "server_script" + } + ], + "modified": "2021-02-18 12:36:19.803425", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 88d68dba14..8838d9e954 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import ast +from typing import Dict, List import frappe from frappe.model.document import Document @@ -14,67 +15,146 @@ from frappe import _ class ServerScript(Document): def validate(self): - frappe.only_for('Script Manager', True) + frappe.only_for("Script Manager", True) + self.validate_script() + self.sync_scheduled_jobs() + self.clear_scheduled_events() + + def on_update(self): + frappe.cache().delete_value("server_script_map") + self.sync_scheduler_events() + + def on_trash(self): + if self.script_type == "Scheduler Event": + for job in self.scheduled_jobs: + frappe.delete_doc("Scheduled Job Type", job.name) + + @property + def scheduled_jobs(self) -> List[Dict[str, str]]: + return frappe.get_all( + "Scheduled Job Type", + filters={"server_script": self.name}, + fields=["name", "stopped"], + ) + + def validate_script(self): + """Utilizes the ast module to check for syntax errors + """ ast.parse(self.script) - @staticmethod - def on_update(): - frappe.cache().delete_value('server_script_map') + def sync_scheduled_jobs(self): + """Sync Scheduled Job Type statuses if Server Script's disabled status is changed + """ + if self.script_type != "Scheduler Event" or not self.has_value_changed("disabled"): + return - def execute_method(self): - if self.script_type == 'API': - # validate if guest is allowed - if frappe.session.user == 'Guest' and not self.allow_guest: - raise frappe.PermissionError - _globals, _locals = safe_exec(self.script) - return _globals.frappe.flags # output can be stored in flags - else: - # wrong report type! + for scheduled_job in self.scheduled_jobs: + if bool(scheduled_job.stopped) != bool(self.disabled): + job = frappe.get_doc("Scheduled Job Type", scheduled_job.name) + job.stopped = self.disabled + job.save() + + def sync_scheduler_events(self): + """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts + """ + if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event": + setup_scheduler_events(script_name=self.name, frequency=self.event_frequency) + + def clear_scheduled_events(self): + """Deletes existing scheduled jobs by Server Script if self.event_frequency has changed + """ + if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"): + for scheduled_job in self.scheduled_jobs: + frappe.delete_doc("Scheduled Job Type", scheduled_job.name) + + def execute_method(self) -> Dict: + """Specific to API endpoint Server Scripts + + Raises: + frappe.DoesNotExistError: If self.script_type is not API + frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user + + Returns: + dict: Evaluates self.script with frappe.utils.safe_exec.safe_exec and returns the flags set in it's safe globals + """ + # wrong report type! + if self.script_type != "API": raise frappe.DoesNotExistError - def execute_doc(self, doc): - # execute event - safe_exec(self.script, None, dict(doc = doc)) + # validate if guest is allowed + if frappe.session.user == "Guest" and not self.allow_guest: + raise frappe.PermissionError + + # output can be stored in flags + _globals, _locals = safe_exec(self.script) + return _globals.frappe.flags + + def execute_doc(self, doc: Document): + """Specific to Document Event triggered Server Scripts + + Args: + doc (Document): Executes script with for a certain document's events + """ + safe_exec(self.script, _locals={"doc": doc}) def execute_scheduled_method(self): - if self.script_type == 'Scheduler Event': - safe_exec(self.script) - else: - # wrong report type! + """Specific to Scheduled Jobs via Server Scripts + + Raises: + frappe.DoesNotExistError: If script type is not a scheduler event + """ + if self.script_type != "Scheduler Event": raise frappe.DoesNotExistError - def get_permission_query_conditions(self, user): + safe_exec(self.script) + + def get_permission_query_conditions(self, user: str) -> List[str]: + """Specific to Permission Query Server Scripts + + Args: + user (str): Takes user email to execute script and return list of conditions + + Returns: + list: Returns list of conditions defined by rules in self.script + """ locals = {"user": user, "conditions": ""} safe_exec(self.script, None, locals) if locals["conditions"]: return locals["conditions"] + @frappe.whitelist() def setup_scheduler_events(script_name, frequency): - method = frappe.scrub('{0}-{1}'.format(script_name, frequency)) - scheduled_script = frappe.db.get_value('Scheduled Job Type', - dict(method=method)) + """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency + + Args: + script_name (str): Name of the Server Script document + frequency (str): Event label compatible with the Frappe scheduler + """ + method = frappe.scrub(f"{script_name}-{frequency}") + scheduled_script = frappe.db.get_value("Scheduled Job Type", {"method": method}) if not scheduled_script: - doc = frappe.get_doc(dict( - doctype = 'Scheduled Job Type', - method = method, - frequency = frequency, - server_script = script_name - )) + frappe.get_doc( + { + "doctype": "Scheduled Job Type", + "method": method, + "frequency": frequency, + "server_script": script_name, + } + ).insert() - doc.insert() - - frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name)) + frappe.msgprint(_("Enabled scheduled execution for script {0}").format(script_name)) else: - doc = frappe.get_doc('Scheduled Job Type', scheduled_script) - doc.update(dict( - doctype = 'Scheduled Job Type', - method = method, - frequency = frequency, - server_script = script_name - )) + doc = frappe.get_doc("Scheduled Job Type", scheduled_script) + + if doc.frequency == frequency: + return + + doc.frequency = frequency doc.save() - frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name)) + frappe.msgprint( + _("Scheduled execution for script {0} has updated").format(script_name) + ) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 8dd6d03fee..aac8b3deed 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -81,7 +81,7 @@ class TestServerScript(unittest.TestCase): def tearDownClass(cls): frappe.db.commit() frappe.db.sql('truncate `tabServer Script`') - frappe.cache().delete_key('server_script_map') + 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 565ee373f1..13dbc32620 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "localization", + "app_name", "country", "language", "column_break_3", @@ -462,6 +463,19 @@ "fieldtype": "Section Break", "label": "Prepared Report" }, + { + "default": "Frappe", + "fieldname": "app_name", + "fieldtype": "Data", + "label": "App Name" + }, + { + "default": "3", + "description": "Hourly rate limit for generating password reset links", + "fieldname": "password_reset_limit", + "fieldtype": "Int", + "label": "Password Reset Link Generation Limit" + }, { "default": "1", "fieldname": "strip_exif_metadata_from_uploaded_images", @@ -472,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", diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index fb1fa4aff9..d16db5fecd 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -2,7 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, unittest +import frappe, unittest, uuid from frappe.model.delete_doc import delete_doc from frappe.utils.data import today, add_to_date @@ -20,6 +20,7 @@ class TestUser(unittest.TestCase): frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0) frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", "") frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 3) + frappe.set_user('Administrator') def test_user_type(self): new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com', @@ -106,13 +107,17 @@ class TestUser(unittest.TestCase): frappe.set_user("testperm@example.com") me = frappe.get_doc("User", "testperm@example.com") - self.assertRaises(frappe.PermissionError, me.add_roles, "System Manager") + me.add_roles("System Manager") + + # system manager is not added (it is reset) + self.assertFalse('System Manager' in [d.role for d in me.roles]) frappe.set_user("Administrator") me = frappe.get_doc("User", "testperm@example.com") me.add_roles("System Manager") + # system manager now added by Administrator self.assertTrue("System Manager" in [d.role for d in me.get("roles")]) # def test_deny_multiple_sessions(self): @@ -235,6 +240,29 @@ class TestUser(unittest.TestCase): self.assertRaises(frappe.ValidationError, user.reset_password, False) + def test_user_rollback(self): + """ """ + frappe.db.commit() + frappe.db.begin() + user_id = str(uuid.uuid4()) + email = f'{user_id}@example.com' + try: + frappe.flags.in_import = True # disable throttling + frappe.get_doc(dict( + doctype='User', + email=email, + first_name=user_id, + )).insert() + finally: + frappe.flags.in_import = False + + # Check user has been added + self.assertIsNotNone(frappe.db.get("User", {"email": email})) + + # Check that rollback works + frappe.db.rollback() + self.assertIsNone(frappe.db.get("User", {"email": email})) + def delete_contact(user): frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user) diff --git a/frappe/core/doctype/user/user.css b/frappe/core/doctype/user/user.css deleted file mode 100644 index ec17030060..0000000000 --- a/frappe/core/doctype/user/user.css +++ /dev/null @@ -1,28 +0,0 @@ -.user-role { - padding: 5px; - width: 50%; - float: left; -} - -table.user-perm { - border-collapse: collapse; - width: 100%; - overflow-x: scroll; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; -} - -table.user-perm td, table.user-perm th { - padding: 5px; - text-align: center; - border-bottom: 1px solid #aaa; - min-width: 30px; -} - -.module-block-list .checkbox { - margin-bottom: 0px; -} - -.module-block-list .checkbox label { - width: 100%; -} diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index b8e16bfe25..3548b4c913 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -27,7 +27,7 @@ frappe.ui.form.on('User', { }, callback: function(data) { frm.set_value("roles", []); - $.each(data.message || [], function(i, v){ + $.each(data.message || [], function(i, v) { var d = frm.add_child("roles"); d.role = v.role; }); @@ -37,16 +37,35 @@ 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(); - if(frm.can_edit_roles && !frm.is_new()) { - if(!frm.roles_editor) { - var role_area = $('
    ') + if (frm.can_edit_roles && !frm.is_new()) { + if (!frm.roles_editor) { + const role_area = $('
    ') .appendTo(frm.fields_dict.roles_html.wrapper); frm.roles_editor = new frappe.RoleEditor(role_area, frm, frm.doc.role_profile_name ? 1 : 0); - var module_area = $('
    ') + var module_area = $('
    ') .appendTo(frm.fields_dict.modules_html.wrapper); frm.module_editor = new frappe.ModuleEditor(frm, module_area); } else { @@ -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 7d91e8cfe0..747ace5de6 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -7,20 +7,20 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "sb0_5", "enabled", "section_break_3", "email", - "first_name", - "middle_name", "last_name", + "language", + "column_break0", + "first_name", "full_name", + "time_zone", + "column_break_11", + "middle_name", + "username", "send_welcome_email", "unsubscribed", - "column_break0", - "username", - "language", - "time_zone", "user_image", "sb1", "role_profile_name", @@ -28,15 +28,17 @@ "roles", "short_bio", "gender", - "phone", - "mobile_no", "birth_date", - "location", - "banner_image", - "column_break_22", "interest", + "banner_image", + "desk_theme", + "column_break_26", + "phone", + "location", "bio", "mute_sounds", + "column_break_22", + "mobile_no", "change_password", "new_password", "logout_all_sessions", @@ -47,13 +49,13 @@ "document_follow_notify", "document_follow_frequency", "email_settings", + "email_signature", "thread_notify", "send_me_a_copy", "allowed_in_mentions", - "email_signature", - "email_inbox", "user_emails", "sb_allow_modules", + "module_profile", "modules_html", "block_modules", "home_settings", @@ -61,15 +63,16 @@ "defaults", "sb3", "simultaneous_sessions", - "user_type", - "login_after", - "login_before", "restrict_ip", - "bypass_restrict_ip_check_if_2fa_enabled", - "column_break1", - "last_login", "last_ip", + "column_break1", + "login_after", + "user_type", "last_active", + "section_break_63", + "login_before", + "bypass_restrict_ip_check_if_2fa_enabled", + "last_login", "last_known_versions", "third_party_authentication", "social_logins", @@ -80,10 +83,6 @@ "api_secret" ], "fields": [ - { - "fieldname": "sb0_5", - "fieldtype": "Section Break" - }, { "default": "1", "fieldname": "enabled", @@ -96,7 +95,8 @@ { "depends_on": "enabled", "fieldname": "section_break_3", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Basic Info" }, { "fieldname": "email", @@ -302,7 +302,7 @@ "no_copy": 1 }, { - "default": "0", + "default": "1", "fieldname": "logout_all_sessions", "fieldtype": "Check", "label": "Logout From All Devices After Changing Password" @@ -577,6 +577,30 @@ "fieldtype": "Password", "label": "API Secret", "read_only": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_63", + "fieldtype": "Column Break" + }, + { + "fieldname": "desk_theme", + "fieldtype": "Select", + "label": "Desk Theme", + "options": "Light\nDark" + }, + { + "fieldname": "module_profile", + "fieldtype": "Link", + "label": "Module Profile", + "options": "Module Profile" } ], "icon": "fa fa-user", @@ -623,11 +647,6 @@ "link_doctype": "User Permission", "link_fieldname": "user" }, - { - "group": "Settings", - "link_doctype": "Assignment Rule", - "link_fieldname": "user" - }, { "group": "Settings", "link_doctype": "Document Follow", @@ -650,7 +669,7 @@ } ], "max_attachments": 5, - "modified": "2020-10-18 15:18:53.126800", + "modified": "2021-02-01 16:11:06.037543", "modified_by": "Administrator", "module": "Core", "name": "User", @@ -678,10 +697,11 @@ } ], "quick_entry": 1, + "route": "user", "search_fields": "full_name", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "title_field": "full_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index da4026d8fd..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...": @@ -85,9 +86,18 @@ class User(Document): def validate_roles(self): if self.role_profile_name: - role_profile = frappe.get_doc('Role Profile', self.role_profile_name) - self.set('roles', []) - self.append_roles(*[role.role for role in role_profile.roles]) + role_profile = frappe.get_doc('Role Profile', self.role_profile_name) + 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: @@ -108,7 +118,7 @@ class User(Document): ) if self.name not in ('Administrator', 'Guest') and not self.user_image: 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) @@ -187,20 +197,17 @@ class User(Document): def share_with_self(self): - if self.user_type=="System User": - frappe.share.add(self.doctype, self.name, self.name, write=1, share=1, - flags={"ignore_share_permission": True}) - else: - frappe.share.remove(self.doctype, self.name, self.name, - flags={"ignore_share_permission": True, "ignore_permissions": True}) + frappe.share.add(self.doctype, self.name, self.name, write=1, share=1, + flags={"ignore_share_permission": True}) def validate_share(self, docshare): - if docshare.user == self.name: - if self.user_type=="System User": - if docshare.share != 1: - frappe.throw(_("Sorry! User should have complete access to their own record.")) - else: - frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) + pass + # if docshare.user == self.name: + # if self.user_type=="System User": + # if docshare.share != 1: + # frappe.throw(_("Sorry! User should have complete access to their own record.")) + # else: + # frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) def send_password_notification(self, new_password): try: @@ -292,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) @@ -555,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) @@ -585,7 +596,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password= frappe.db.set_value("User", user, "reset_password_key", "") if user_doc.user_type == "System User": - return "/desk" + return "/app" else: return redirect_url if redirect_url else "/" @@ -1006,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 @@ -1042,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) @@ -1102,7 +1123,6 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False): contact.save(ignore_permissions=True) - @frappe.whitelist() def generate_keys(user): """ @@ -1123,6 +1143,11 @@ def generate_keys(user): return {"api_secret": api_secret} frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) +@frappe.whitelist() +def switch_theme(theme): + if theme in ["Dark", "Light"]: + frappe.db.set_value("User", frappe.session.user, "desk_theme", theme) + def update_password_reset_limit(user): generated_link_count = get_generated_link_count(user) generated_link_count += 1 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 ba14583c2f..fbc788f6bf 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -49,13 +49,14 @@ 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) frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow)) -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def get_user_permissions(user=None): '''Get all users permissions for the user as a dict of doctype''' # if this is called from client-side, @@ -66,7 +67,7 @@ def get_user_permissions(user=None): if not user: user = frappe.session.user - if not user or user == "Administrator": + if not user or user in ("Administrator", "Guest"): return {} cached_user_permissions = frappe.cache().hget("user_permissions", user) @@ -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 3e822f0007..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", @@ -145,7 +163,7 @@ frappe.listview_settings['User Permission'] = { } frappe.show_alert({ message, - indicator: 'green' + indicator: 'info' }); list_view.refresh(); }); @@ -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/page/background_jobs/background_jobs.css b/frappe/core/page/background_jobs/background_jobs.css new file mode 100644 index 0000000000..0c77522cb3 --- /dev/null +++ b/frappe/core/page/background_jobs/background_jobs.css @@ -0,0 +1,60 @@ +.list-jobs { + font-size: var(--text-base); +} + +.table { + margin-bottom: 0px; + margin-top: 0px; +} + +thead { + background-color: var(--control-bg); + border-radius: var(--border-radius-sm); +} + +thead > tr { + border-radius: var(--border-radius-sm); +} + +thead > tr > th:first-child { + border-radius: var(--border-radius-sm) 0 0 var(--border-radius-sm); +} +thead > tr > th:last-child { + border-radius: 0 var(--border-radius-sm) var(--border-radius-sm) 0; +} + +.worker-name { + display: flex; + align-items: center; +} + +.job-name { + font-size: var(--text-md); + font-family: "Courier New", Courier, monospace; + /* background-color: var(--control-bg); */ + /* padding: var(--padding-xs) var(--padding-sm); */ + /* border-radius: var(--border-radius-md); */ +} + +.background-job-row:hover { + background-color: var(--bg-color); +} + +.no-background-jobs { + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.no-background-jobs > img { + margin-bottom: var(--margin-md); + max-height: 100px; +} + +.footer { + align-items: flex-end; + margin-top: var(--margin-md); + font-size: var(--text-base); +} diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index c5d598ccd3..1b00ec3106 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -1,6 +1,6 @@
    {% if jobs.length %} - +
    @@ -11,30 +11,41 @@ {% for j in jobs %} - + - + {% endfor %}
    {{ __("Queue / Worker") }}
    {{ j.queue.split(".").slice(-1)[0] }} + + {{ j.queue.split(".").slice(-1)[0] }} +
    - {{ frappe.utils.encode_tags(j.job_name) }} + + {{ frappe.utils.encode_tags(j.job_name) }} +
    {% if j.exc_info %} -
    +
    {{ frappe.utils.encode_tags(j.exc_info) }}
    {% endif %}
    {{ j.creation }}{{ j.creation }}
    -

    - {{ __("Started") }} - {{ __("Queued") }} - {{ __("Failed") }} - {{ __("Finished") }} -

    {% else %} -

    {{ __("No pending or current jobs for this site") }}

    +
    + Empty State +

    {{ __("No pending or current jobs for this site") }}

    +
    {% endif %} -

    {{ __("Last refreshed") }} {{ frappe.datetime.now_datetime() }}

    -
    + +
    \ 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 bbc8bf049b..cabe91375f 100644 --- a/frappe/core/page/background_jobs/background_jobs.js +++ b/frappe/core/page/background_jobs/background_jobs.js @@ -1,45 +1,65 @@ -frappe.pages['background_jobs'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ - parent: wrapper, - title: __('Background Jobs'), - single_column: true +frappe.pages["background_jobs"].on_page_load = (wrapper) => { + const background_job = new BackgroundJobs(wrapper); + + $(wrapper).bind('show', () => { + background_job.show(); }); - $(frappe.render_template('background_jobs_outer')).appendTo(page.body); - page.content = $(page.body).find('.table-area'); + window.background_jobs = background_job; +}; - frappe.pages.background_jobs.page = page; -} +class BackgroundJobs { + constructor(wrapper) { + this.page = frappe.ui.make_app_page({ + parent: wrapper, + title: __('Background Jobs'), + single_column: true + }); -frappe.pages['background_jobs'].on_page_show = function(wrapper) { - frappe.pages.background_jobs.refresh_jobs(); - frappe.call({ - method: 'frappe.core.page.background_jobs.background_jobs.get_scheduler_status', - callback: function(r) { - frappe.pages.background_jobs.page.set_indicator(...r.message); - } - }); -} + this.called = false; + this.show_failed = false; -frappe.pages.background_jobs.refresh_jobs = function() { - var page = frappe.pages.background_jobs.page; - - // don't call if already waiting for a response - if(page.called) return; - page.called = true; - frappe.call({ - method: 'frappe.core.page.background_jobs.background_jobs.get_info', - args: { - show_failed: page.body.find('.show-failed').prop('checked') ? 1 : 0 - }, - callback: function(r) { - page.called = false; - page.body.find('.list-jobs').remove(); - $(frappe.render_template('background_jobs', {jobs:r.message || []})).appendTo(page.content); - - if(frappe.get_route()[0]==='background_jobs') { - frappe.background_jobs_timeout = setTimeout(frappe.pages.background_jobs.refresh_jobs, 2000); + this.show_failed_button = this.page.add_inner_button(__("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); + this.content = $(this.page.body).find('.table-area'); + } + + show() { + this.refresh_jobs(); + frappe.call({ + method: 'frappe.core.page.background_jobs.background_jobs.get_scheduler_status', + callback: res => { + this.page.set_indicator(...res.message); + } + }); + } + + refresh_jobs() { + if (this.called) return; + this.called = true; + + frappe.call({ + method: 'frappe.core.page.background_jobs.background_jobs.get_info', + args: { + show_failed: this.show_failed + }, + callback: (res) => { + this.called = false; + this.page.body.find('.list-jobs').remove(); + $(frappe.render_template('background_jobs', { jobs: res.message || [] })).appendTo(this.content); + + if (frappe.get_route()[0] === 'background_jobs') { + setTimeout(() => this.refresh_jobs(), 2000); + } + } + }); + } +} \ No newline at end of file diff --git a/frappe/core/page/background_jobs/background_jobs_outer.html b/frappe/core/page/background_jobs/background_jobs_outer.html index 4da4498304..4ca3a32906 100644 --- a/frappe/core/page/background_jobs/background_jobs_outer.html +++ b/frappe/core/page/background_jobs/background_jobs_outer.html @@ -1,11 +1,4 @@ -
    -

    -

    - -
    -

    +
    diff --git a/frappe/core/page/desktop/__init__.py b/frappe/core/page/dashboard_view/__init__.py similarity index 100% rename from frappe/core/page/desktop/__init__.py rename to frappe/core/page/dashboard_view/__init__.py diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard_view/dashboard_view.js similarity index 92% rename from frappe/core/page/dashboard/dashboard.js rename to frappe/core/page/dashboard_view/dashboard_view.js index 7e45163a7e..686d11c6bf 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard_view/dashboard_view.js @@ -5,7 +5,7 @@ frappe.provide('frappe.dashboards'); frappe.provide('frappe.dashboards.chart_sources'); -frappe.pages['dashboard'].on_page_load = function(wrapper) { +frappe.pages['dashboard-view'].on_page_load = function(wrapper) { frappe.ui.make_app_page({ parent: wrapper, title: __("Dashboard"), @@ -21,7 +21,7 @@ frappe.pages['dashboard'].on_page_load = function(wrapper) { class Dashboard { constructor(wrapper) { this.wrapper = $(wrapper); - $(`
    + $(`
    `).appendTo(this.wrapper.find(".page-content").empty()); this.container = this.wrapper.find(".dashboard-graph"); @@ -36,17 +36,17 @@ class Dashboard { } else { // last opened if (frappe.last_dashboard) { - frappe.set_route('dashboard', frappe.last_dashboard); + frappe.set_route('dashboard-view', frappe.last_dashboard); } else { // default dashboard frappe.db.get_list('Dashboard', {filters: {is_default: 1}}).then(data => { if (data && data.length) { - frappe.set_route('dashboard', data[0].name); + frappe.set_route('dashboard-view', data[0].name); } else { // no default, get the latest one frappe.db.get_list('Dashboard', {limit: 1}).then(data => { if (data && data.length) { - frappe.set_route('dashboard', data[0].name); + frappe.set_route('dashboard-view', data[0].name); } else { // create a new dashboard! frappe.new_doc('Dashboard'); @@ -183,8 +183,8 @@ class Dashboard { frappe.db.get_list('Dashboard').then(dashboards => { dashboards.map(dashboard => { let name = dashboard.name; - if(name != this.dashboard_name){ - this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1); + if (name != this.dashboard_name) { + this.page.add_menu_item(name, () => frappe.set_route("dashboard-view", name), 1); } }); }); diff --git a/frappe/core/page/dashboard/dashboard.json b/frappe/core/page/dashboard_view/dashboard_view.json similarity index 74% rename from frappe/core/page/dashboard/dashboard.json rename to frappe/core/page/dashboard_view/dashboard_view.json index 58fda5a34c..4ece98a779 100644 --- a/frappe/core/page/dashboard/dashboard.json +++ b/frappe/core/page/dashboard_view/dashboard_view.json @@ -4,12 +4,12 @@ "docstatus": 0, "doctype": "Page", "idx": 0, - "modified": "2020-03-26 13:30:44.603948", + "modified": "2020-12-16 12:29:08.610352", "modified_by": "Administrator", "module": "Core", - "name": "dashboard", + "name": "dashboard-view", "owner": "Administrator", - "page_name": "Dashboard", + "page_name": "dashboard-view", "roles": [], "script": null, "standard": "Yes", 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.css b/frappe/core/page/permission_manager/permission_manager.css new file mode 100644 index 0000000000..fec486aa81 --- /dev/null +++ b/frappe/core/page/permission_manager/permission_manager.css @@ -0,0 +1,51 @@ +.table { + margin-bottom: 0px; + margin-top: 0px; + border-radius: var(--border-radius-md); +} + +thead { + border: none; + background-color: var(--control-bg); + border-radius: var(--border-radius-md); +} + +thead > tr { + border-radius: var(--border-radius-md); +} + +thead > tr > th:first-child { + border-radius: var(--border-radius-md) 0 0 var(--border-radius-md); +} +thead > tr > th:last-child { + border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0; +} + +/* Space between thead and tbody */ +/* tbody:before { + content: "@"; + display: block; + line-height: var(--margin-md); + text-indent: -99999px; +} */ + +td[data-fieldname="permissions"] > .row > .col-md-4 { + margin-bottom: var(--margin-sm); +} + +tbody > tr { + border-top: 1px solid var(--border-color); +} + +tbody > tr:first-child { + border-top: none; +} + +button.btn-remove-perm { + box-shadow: none; + padding: var(--padding-xs) var(--padding-xs); +} + +button.btn-remove-perm > svg > use { + stroke: var(--white); +} diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 02fbf943d5..41cc900a97 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -1,8 +1,8 @@ frappe.pages['permission-manager'].on_page_load = (wrapper) => { - var page = frappe.ui.make_app_page({ + let page = frappe.ui.make_app_page({ parent: wrapper, title: __('Role Permissions Manager'), - icon: "fa fa-lock", + card_layout: true, single_column: true }); @@ -14,233 +14,253 @@ frappe.pages['permission-manager'].on_page_load = (wrapper) => { }; -frappe.pages['permission-manager'].refresh = function(wrapper) { +frappe.pages['permission-manager'].refresh = function (wrapper) { wrapper.permission_engine.set_from_route(); }; -frappe.PermissionEngine = Class.extend({ - init: function(wrapper) { +frappe.PermissionEngine = class PermissionEngine { + constructor(wrapper) { this.wrapper = wrapper; this.page = wrapper.page; this.body = $(this.wrapper).find(".perm-engine"); this.make(); this.refresh(); this.add_check_events(); - }, - make: function() { - var me = this; + } - me.make_reset_button(); - return frappe.call({ - module:"frappe.core", - page:"permission_manager", - method: "get_roles_and_doctypes", - callback: function(r) { - me.options = r.message; - me.setup_page(); - } + make() { + this.make_reset_button(); + frappe.call({ + module: "frappe.core", + page: "permission_manager", + method: "get_roles_and_doctypes" + }).then((res) => { + this.options = res.message; + this.setup_page(); }); + } - }, - setup_page: function() { - var me = this; + setup_page() { this.doctype_select = this.wrapper.page.add_select(__("Document Type"), - [{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes)) - .change(function() { + [{ value: "", label: __("Select Document Type") + "..." }].concat(this.options.doctypes)) + .change(function () { frappe.set_route("permission-manager", $(this).val()); }); + this.role_select = this.wrapper.page.add_select(__("Roles"), - [__("Select Role")+"..."].concat(this.options.roles)) - .change(function() { - me.refresh(); + [__("Select Role") + "..."].concat(this.options.roles)) + .change(() => { + this.refresh(); }); this.page.add_inner_button(__('Set User Permissions'), () => { return frappe.set_route('List', 'User Permission'); }); this.set_from_route(); - }, - set_from_route: function() { - var me = this; - if(!this.doctype_select) { + } + + set_from_route() { + if (!this.doctype_select) { // selects not yet loaded, call again after a bit setTimeout(() => { - me.set_from_route(); + this.set_from_route(); }, 500); return; } - if(frappe.get_route()[1]) { + if (frappe.get_route()[1]) { this.doctype_select.val(frappe.get_route()[1]); - } else if(frappe.route_options) { - if(frappe.route_options.doctype) { + } else if (frappe.route_options) { + if (frappe.route_options.doctype) { this.doctype_select.val(frappe.route_options.doctype); } - if(frappe.route_options.role) { + if (frappe.route_options.role) { this.role_select.val(frappe.route_options.role); } frappe.route_options = null; } this.refresh(); - }, - get_standard_permissions: function(callback) { - var doctype = this.get_doctype(); - if(doctype) { + } + + get_standard_permissions(callback) { + let doctype = this.get_doctype(); + if (doctype) { return frappe.call({ - module:"frappe.core", - page:"permission_manager", + module: "frappe.core", + page: "permission_manager", method: "get_standard_permissions", - args: {doctype: doctype}, + args: { doctype: doctype }, callback: callback }); } return false; - }, - reset_std_permissions: function(data) { - var me = this; - var d = frappe.confirm(__("Reset Permissions for {0}?", [me.get_doctype()]), function() { + } + + reset_std_permissions(data) { + let doctype = this.get_doctype(); + let d = frappe.confirm(__("Reset Permissions for {0}?", [doctype]), () => { return frappe.call({ - module:"frappe.core", - page:"permission_manager", - method:"reset", - args: { - doctype: me.get_doctype(), - }, - callback: function() { - me.refresh(); - } + module: "frappe.core", + page: "permission_manager", + method: "reset", + args: { doctype } + }).then(() => { + this.refresh(); }); }); // show standard permissions - var $d = $(d.wrapper).find(".frappe-confirm-message").append("

    Standard Permissions:


    "); - var $wrapper = $("

    ").appendTo($d); - $.each(data.message, function(i, d) { - d.rights = []; - $.each(me.rights, function(i, r) { - if(d[r]===1) { - d.rights.push(__(toTitle(r.replace("_", " ")))); - } - }); - d.rights = d.rights.join(", "); - $wrapper.append(repl('
    \ -
    %(role)s, Level %(permlevel)s
    \ -
    %(rights)s
    \ -

    ', d)); - }); + let $d = $(d.wrapper).find(".frappe-confirm-message").append("
    Standard Permissions:

    "); + let $wrapper = $("

    ").appendTo($d); + data.message.forEach((d) => { + let rights = this.rights + .filter((r) => d[r]) + .map((r) => { + return __(toTitle(frappe.unscrub(r))); + }); - }, - get_doctype: function() { - var doctype = this.doctype_select.val(); - return this.doctype_select.get(0).selectedIndex==0 ? null : doctype; - }, - get_role: function() { - var role = this.role_select.val(); - return this.role_select.get(0).selectedIndex==0 ? null : role; - }, - refresh: function() { - var me = this; - if(!me.doctype_select) { - this.body.html("

    " + __("Loading") + "...

    "); - return; + d.rights = rights.join(", "); + + $wrapper.append(`
    \ +
    ${d.role}, Level ${d.permlevel || 0}
    \ +
    ${d.rights}
    \ +

    `); + }); + } + + get_doctype() { + let doctype = this.doctype_select.val(); + return this.doctype_select.get(0).selectedIndex == 0 ? null : doctype; + } + + get_role() { + let role = this.role_select.val(); + return this.role_select.get(0).selectedIndex == 0 ? null : role; + } + + set_empty_message(message) { + this.body.html(` +
    +

    + ${message} +

    +
    `); + } + + refresh() { + this.page.clear_secondary_action(); + this.page.clear_primary_action(); + + if (!this.doctype_select) { + return this.set_empty_message(__("Loading")); } - if(!me.get_doctype() && !me.get_role()) { - this.body.html("

    "+__("Select Document Type or Role to start.")+"

    "); - return; + + let doctype = this.get_doctype(); + let role = this.get_role(); + + if (!doctype && !role) { + return this.set_empty_message(__("Select Document Type or Role to start.")); } + // get permissions frappe.call({ module: "frappe.core", page: "permission_manager", method: "get_permissions", - args: { - doctype: me.get_doctype(), - role: me.get_role() - }, - callback: function(r) { - me.render(r.message); - } + args: { doctype, role } + }).then((r) => { + this.render(r.message); }); - }, - render: function(perm_list) { + } + + render(perm_list) { this.body.empty(); this.perm_list = perm_list || []; - if(!this.perm_list.length) { - this.body.html("

    " - +__("No Permissions set for this criteria.")+"

    "); + if (!this.perm_list.length) { + this.set_empty_message(__("No Permissions set for this criteria.")); } else { this.show_permission_table(this.perm_list); } this.show_add_rule(); - this.make_reset_button(); - }, - show_permission_table: function(perm_list) { + this.get_doctype() && this.make_reset_button(); + } - var me = this; + show_permission_table(perm_list) { this.table = $("
    \ - \ +
    \ \ \
    \
    ").appendTo(this.body); - $.each([[__("Document Type"), 150], [__("Role"), 170], [__("Level"), 40], - [__("Permissions"), 350], ["", 40]], function(i, col) { - $("").html(col[0]).css("width", col[1]+"px") - .appendTo(me.table.find("thead tr")); + const table_columns = [ + [__("Document Type"), 150], + [__("Role"), 170], + [__("Level"), 40], + [__("Permissions"), 350], + ["", 40] + ]; + + table_columns.forEach((col) => { + $("") + .html(col[0]) + .css("width", col[1] + "px") + .appendTo(this.table.find("thead tr")); }); - $.each(perm_list, function(i, d) { - if(d.parent==="DocType") { + perm_list.forEach((d) => { + if (d.parent === "DocType") { return; } - if(!d.permlevel) d.permlevel = 0; - var row = $("").appendTo(me.table.find("tbody")); - me.add_cell(row, d, "parent"); - var role_cell = me.add_cell(row, d, "role"); - me.set_show_users(role_cell, d.role); - if (d.permlevel===0) { - // me.setup_user_permissions(d, role_cell); - me.setup_if_owner(d, role_cell); + if (!d.permlevel) d.permlevel = 0; + + let row = $("").appendTo(this.table.find("tbody")); + this.add_cell(row, d, "parent"); + let role_cell = this.add_cell(row, d, "role"); + + this.set_show_users(role_cell, d.role); + + if (d.permlevel === 0) { + // this.setup_user_permissions(d, role_cell); + this.setup_if_owner(d, role_cell); } - var cell = me.add_cell(row, d, "permlevel"); - if(d.permlevel==0) { + let cell = this.add_cell(row, d, "permlevel"); + + if (d.permlevel == 0) { cell.css("font-weight", "bold"); - row.addClass("warning"); } - var perm_cell = me.add_cell(row, d, "permissions").css("padding-top", 0); - var perm_container = $("
    ").appendTo(perm_cell); + let perm_cell = this.add_cell(row, d, "permissions"); + let perm_container = $("
    ").appendTo(perm_cell); - me.rights.forEach(r => { + this.rights.forEach(r => { if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return; if (d.in_create && ['create', 'write', 'delete'].includes(r)) return; - me.add_check(perm_container, d, r); + this.add_check(perm_container, d, r); }); // buttons - me.add_delete_button(row, d); + this.add_delete_button(row, d); }); - }, + } - add_cell: function(row, d, fieldname) { + add_cell(row, d, fieldname) { return $("").appendTo(row) .attr("data-fieldname", fieldname) + .addClass("pt-4") .html(__(d[fieldname])); - }, + } - add_check: (cell, d, fieldname, label, description="") => { - var me = this; - - if(!label) label = toTitle(fieldname.replace(/_/g, " ")); - if(d.permlevel > 0 && ["read", "write"].indexOf(fieldname)==-1) { + add_check(cell, d, fieldname, label, description = "") { + if (!label) label = toTitle(fieldname.replace(/_/g, " ")); + if (d.permlevel > 0 && ["read", "write"].indexOf(fieldname) == -1) { return; } - var checkbox = $( + let checkbox = $( `
    @@ -251,7 +271,7 @@ frappe.PermissionEngine = Class.extend({ .attr("data-fieldname", fieldname); checkbox.find("input") - .prop("checked", d[fieldname] ? true: false) + .prop("checked", d[fieldname] ? true : false) .attr("data-ptype", fieldname) .attr("data-role", d.role) .attr("data-permlevel", d.permlevel) @@ -261,23 +281,25 @@ frappe.PermissionEngine = Class.extend({ .css("text-transform", "capitalize"); return checkbox; - }, + } - setup_if_owner: function(d, role_cell) { + setup_if_owner(d, role_cell) { this.add_check(role_cell, d, "if_owner", "Only If Creator") .removeClass("col-md-4") - .css({"margin-top": "15px"}); - }, + .css({ "margin-top": "15px" }); + } - rights: ["select", "read", "write", "create", "delete", "submit", "cancel", "amend", - "print", "email", "report", "import", "export", "set_user_permissions", "share"], + get rights() { + return ["select", "read", "write", "create", "delete", "submit", "cancel", "amend", + "print", "email", "report", "import", "export", "set_user_permissions", "share"]; + } - set_show_users: function(cell, role) { - cell.html(""+__(role)+"") + set_show_users(cell, role) { + cell.html("" + __(role) + "") .find("a") .attr("data-role", role) - .click(function() { - var role = $(this).attr("data-role"); + .click(function () { + let role = $(this).attr("data-role"); frappe.call({ module: "frappe.core", page: "permission_manager", @@ -285,9 +307,9 @@ frappe.PermissionEngine = Class.extend({ args: { role: role }, - callback: function(r) { - r.message = $.map(r.message, function(p) { - return $.format('{1}', [p, p]); + callback: function (r) { + r.message = $.map(r.message, function (p) { + return $.format('{1}', [p, p]); }); frappe.msgprint(__("Users with role {0}:", [__(role)]) + "
    " + r.message.join("
    ")); @@ -295,16 +317,15 @@ frappe.PermissionEngine = Class.extend({ }); return false; }); - }, + } - add_delete_button: function(row, d) { - var me = this; - $("") - .appendTo($("").appendTo(row)) + add_delete_button(row, d) { + $(``) + .appendTo($(``).appendTo(row)) .attr("data-doctype", d.parent) .attr("data-role", d.role) .attr("data-permlevel", d.permlevel) - .click(function() { + .click(function () { return frappe.call({ module: "frappe.core", page: "permission_manager", @@ -314,29 +335,27 @@ frappe.PermissionEngine = Class.extend({ role: $(this).attr("data-role"), permlevel: $(this).attr("data-permlevel") }, - callback: function(r) { - if(r.exc) { + callback: (r) => { + if (r.exc) { frappe.msgprint(__("Did not remove")); } else { - me.refresh(); + this.refresh(); } } }); }); - }, + } - add_check_events: function() { - var me = this; - - this.body.on("click", ".show-user-permissions", function() { - frappe.route_options = { allow: me.get_doctype() || "" }; + add_check_events() { + this.body.on("click", ".show-user-permissions", () => { + frappe.route_options = { allow: this.get_doctype() || "" }; frappe.set_route('List', 'User Permission'); }); - this.body.on("click", "input[type='checkbox']", function() { + this.body.on("click", "input[type='checkbox']", function () { frappe.dom.freeze(); - var chk = $(this); - var args = { + let chk = $(this); + let args = { role: chk.attr("data-role"), permlevel: chk.attr("data-permlevel"), doctype: chk.attr("data-doctype"), @@ -348,49 +367,53 @@ frappe.PermissionEngine = Class.extend({ page: "permission_manager", method: "update", args: args, - callback: function(r) { + callback: (r) => { frappe.dom.unfreeze(); - if(r.exc) { + if (r.exc) { // exception: reverse chk.prop("checked", !chk.prop("checked")); } else { - me.get_perm(args.role)[args.ptype]=args.value; + this.get_perm(args.role)[args.ptype] = args.value; } } }); }); - }, + } - show_add_rule: function() { - var me = this; - $("") - .appendTo($("

    ").appendTo(this.body)) - .click(function() { - var d = new frappe.ui.Dialog({ + show_add_rule() { + this.page.set_primary_action( + __("Add A New Rule"), + () => { + let d = new frappe.ui.Dialog({ title: __("Add New Permission Rule"), fields: [ - {fieldtype:"Select", label:__("Document Type"), - options:me.options.doctypes, reqd:1, fieldname:"parent"}, - {fieldtype:"Select", label:__("Role"), - options:me.options.roles, reqd:1,fieldname:"role"}, - {fieldtype:"Select", label:__("Permission Level"), - options:[0,1,2,3,4,5,6,7,8,9], reqd:1, fieldname: "permlevel", - description: __("Level 0 is for document level permissions, higher levels for field level permissions.")} + { + fieldtype: "Select", label: __("Document Type"), + options: this.options.doctypes, reqd: 1, fieldname: "parent" + }, + { + fieldtype: "Select", label: __("Role"), + options: this.options.roles, reqd: 1, fieldname: "role" + }, + { + fieldtype: "Select", label: __("Permission Level"), + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], reqd: 1, fieldname: "permlevel", + description: __("Level 0 is for document level permissions, higher levels for field level permissions.") + } ] }); - if(me.get_doctype()) { - d.set_value("parent", me.get_doctype()); + if (this.get_doctype()) { + d.set_value("parent", this.get_doctype()); d.get_input("parent").prop("disabled", true); } - if(me.get_role()) { - d.set_value("role", me.get_role()); + if (this.get_role()) { + d.set_value("role", this.get_role()); d.get_input("role").prop("disabled", true); } d.set_value("permlevel", "0"); - d.set_primary_action(__('Add'), function() { - var args = d.get_values(); - if(!args) { + d.set_primary_action(__('Add'), () => { + let args = d.get_values(); + if (!args) { return; } frappe.call({ @@ -398,40 +421,40 @@ frappe.PermissionEngine = Class.extend({ page: "permission_manager", method: "add", args: args, - callback: function(r) { - if(r.exc) { + callback: (r) => { + if (r.exc) { frappe.msgprint(__("Did not add")); } else { - me.refresh(); + this.refresh(); } } }); d.hide(); }); d.show(); - }); - }, + }, + "small-add" + ); + } - make_reset_button: function() { - var me = this; - $('') - .appendTo(this.body.find(".permission-toolbar")) - .on("click", function() { - me.get_standard_permissions(function(data) { - me.reset_std_permissions(data); + make_reset_button() { + this.page.set_secondary_action( + __("Restore Original Permissions"), + () => { + this.get_standard_permissions((data) => { + this.reset_std_permissions(data); }); }); - }, - - get_perm: function(role) { - return $.map(this.perm_list, function(d) { - if(d.role==role) return d; - })[0]; - }, - - get_link_fields: function(doctype) { - return frappe.get_children("DocType", doctype, "fields", - {fieldtype:"Link", options:["not in", ["User", '[Select]']]}); } -}); + + get_perm(role) { + return $.map(this.perm_list, function (d) { + if (d.role == role) return d; + })[0]; + } + + get_link_fields(doctype) { + return frappe.get_children("DocType", doctype, "fields", + { fieldtype: "Link", options: ["not in", ["User", '[Select]']] }); + } +}; diff --git a/frappe/core/page/permission_manager/permission_manager_help.html b/frappe/core/page/permission_manager/permission_manager_help.html index f192244fc9..0613713e81 100644 --- a/frappe/core/page/permission_manager/permission_manager_help.html +++ b/frappe/core/page/permission_manager/permission_manager_help.html @@ -1,12 +1,12 @@


    -
    +

    {%= __("Quick Help for Setting Permissions") %}:

    1. {%= __("Permissions are set on Roles and Document Types (called DocTypes) by setting rights like Read, Write, Create, Delete, Submit, Cancel, Amend, Report, Import, Export, Print, Email and Set User Permissions.") %}
    2. {%= __("Permissions get applied on Users based on what Roles they are assigned.") %}
    3. {%= __("Roles can be set for users from their User page.") %} - {%= __("Setup > User") %}
    4. -
    5. {%= __("The system provides many pre-defined roles. You can add new roles to set finer permissions.") %} {%= __("Add a New Role") %}
    6. + {%= __("Setup > User") %} +
    7. {%= __("The system provides many pre-defined roles. You can add new roles to set finer permissions.") %} {%= __("Add a New Role") %}
    8. {%= __("Permissions are automatically applied to Standard Reports and searches.") %}
    9. {%= __("As a best practice, do not assign the same set of permission rule to different Roles. Instead, set multiple Roles to the same User.") %}
    @@ -24,13 +24,13 @@
  • {%= __("Permissions at level 0 are Document Level permissions, i.e. they are primary for access to the document.") %}
  • {%= __("If a Role does not have access at Level 0, then higher levels are meaningless.") %}
  • {%= __("Permissions at higher levels are Field Level permissions. All Fields have a Permission Level set against them and the rules defined at that permissions apply to the field. This is useful in case you want to hide or make certain field read-only for certain Roles.") %}
  • -
  • {%= __("You can use Customize Form to set levels on fields.") %} {%= __("Setup > Customize Form") %}
  • +
  • {%= __("You can use Customize Form to set levels on fields.") %} {%= __("Setup > Customize Form") %}

  • {%= __("User Permissions") %}:

    1. {%= __("User Permissions are used to limit users to specific records.") %} - {%= __("Setup > User Permissions") %}
    2. + {%= __("Setup > User Permissions") %}
    3. {%= __("Select Document Types to set which User Permissions are used to limit access.") %}
    4. {%= __("Once you have set this, the users will only be able access documents (eg. Blog Post) where the link exists (eg. Blogger).") %}
    5. {%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}
    6. 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/workspace/workspace.js b/frappe/core/page/workspace/workspace.js deleted file mode 100644 index 78256e3acd..0000000000 --- a/frappe/core/page/workspace/workspace.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.pages['workspace'].on_page_load = function(wrapper) { - frappe.utils.set_title(__("Home")); -} \ No newline at end of file diff --git a/frappe/core/page/workspace/workspace.json b/frappe/core/page/workspace/workspace.json deleted file mode 100644 index 49de4a8e03..0000000000 --- a/frappe/core/page/workspace/workspace.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-02-27 15:07:57.124916", - "modified_by": "Administrator", - "module": "Core", - "name": "workspace", - "owner": "Administrator", - "page_name": "workspace", - "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..aefda698b1 --- /dev/null +++ b/frappe/core/workspace/build/build.json @@ -0,0 +1,212 @@ +{ + "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_default": 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": "Client Script", + "link_to": "Client 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-02-04 13:48:48.493146", + "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/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json new file mode 100644 index 0000000000..fb26b73cfc --- /dev/null +++ b/frappe/core/workspace/settings/settings.json @@ -0,0 +1,367 @@ +{ + "category": "Modules", + "charts": [], + "creation": "2020-03-02 15:09:40.527211", + "developer_mode_only": 0, + "disable_user_customization": 1, + "docstatus": 0, + "doctype": "Workspace", + "extends_another_page": 0, + "hide_custom": 0, + "icon": "setting", + "idx": 0, + "is_standard": 1, + "label": "Settings", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Data", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Import Data", + "link_to": "Data Import", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Export Data", + "link_to": "Data Export", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Bulk Update", + "link_to": "Bulk Update", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Download Backups", + "link_to": "backups", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Deleted Documents", + "link_to": "Deleted Document", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email / Notifications", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Account", + "link_to": "Email Account", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Domain", + "link_to": "Email Domain", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Notification", + "link_to": "Notification", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Template", + "link_to": "Email Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Auto Email Report", + "link_to": "Auto Email Report", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Notification Settings", + "link_to": "Notification Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Website", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Settings", + "link_to": "Website Settings", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Theme", + "link_to": "Website Theme", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Website Script", + "link_to": "Website Script", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "About Us Settings", + "link_to": "About Us Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Contact Us Settings", + "link_to": "Contact Us Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Core", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "System Settings", + "link_to": "System Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Error Log", + "link_to": "Error Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Error Snapshot", + "link_to": "Error Snapshot", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Domain Settings", + "link_to": "Domain Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Printing", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Format Builder", + "link_to": "print-format-builder", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Settings", + "link_to": "Print Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Format", + "link_to": "Print Format", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Print Style", + "link_to": "Print Style", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_to": "Workflow", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow State", + "link_to": "Workflow State", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workflow Action", + "link_to": "Workflow Action", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2020-12-01 13:38:40.235323", + "modified_by": "Administrator", + "module": "Core", + "name": "Settings", + "owner": "Administrator", + "pin_to_bottom": 1, + "pin_to_top": 0, + "shortcuts": [ + { + "icon": "setting", + "label": "System Settings", + "link_to": "System Settings", + "type": "DocType" + }, + { + "icon": "printer", + "label": "Print Settings", + "link_to": "Print Settings", + "type": "DocType" + }, + { + "icon": "website", + "label": "Website Settings", + "link_to": "Website Settings", + "type": "DocType" + } + ], + "shortcuts_label": "Settings" +} \ No newline at end of file diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json new file mode 100644 index 0000000000..05746a00c2 --- /dev/null +++ b/frappe/core/workspace/users/users.json @@ -0,0 +1,167 @@ +{ + "category": "Administration", + "charts": [], + "creation": "2020-03-02 15:12:16.754449", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Workspace", + "extends_another_page": 0, + "hide_custom": 0, + "icon": "users", + "idx": 0, + "is_standard": 1, + "label": "Users", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Users", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "User", + "link_to": "User", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role", + "link_to": "Role", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Profile", + "link_to": "Role Profile", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Logs", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Activity Log", + "link_to": "Activity Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Access Log", + "link_to": "Access Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Permissions", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Permissions Manager", + "link_to": "permission-manager", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "User Permissions", + "link_to": "User Permission", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Permission for Page and Report", + "link_to": "Role Permission for Page and Report", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "User", + "hidden": 0, + "is_query_report": 1, + "label": "Permitted Documents For User", + "link_to": "Permitted Documents For User", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "DocShare", + "hidden": 0, + "is_query_report": 0, + "label": "Document Share Report", + "link_to": "Document Share Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2020-12-01 13:38:40.085519", + "modified_by": "Administrator", + "module": "Core", + "name": "Users", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "label": "User", + "link_to": "User", + "type": "DocType" + }, + { + "label": "Role", + "link_to": "Role", + "type": "DocType" + }, + { + "label": "Permission Manager", + "link_to": "permission-manager", + "type": "Page" + }, + { + "label": "User Profile", + "link_to": "user-profile", + "type": "Page" + } + ] +} \ No newline at end of file diff --git a/frappe/custom/desk_page/customization/customization.json b/frappe/custom/desk_page/customization/customization.json deleted file mode 100644 index 29f4cb745f..0000000000 --- a/frappe/custom/desk_page/customization/customization.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "cards": [ - { - "hidden": 0, - "label": "Dashboards", - "links": "[\n {\n \"label\": \"Dashboard\",\n \"name\": \"Dashboard\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart\",\n \"name\": \"Dashboard Chart\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart Source\",\n \"name\": \"Dashboard Chart Source\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Form Customization", - "links": "[\n {\n \"description\": \"Change field properties (hide, readonly, permission etc.)\",\n \"label\": \"Customize Form\",\n \"name\": \"Customize Form\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add fields to forms.\",\n \"label\": \"Custom Field\",\n \"name\": \"Custom Field\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom javascript to forms.\",\n \"label\": \"Custom Script\",\n \"name\": \"Custom Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom forms.\",\n \"label\": \"DocType\",\n \"name\": \"DocType\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Other", - "links": "[\n {\n \"description\": \"Add your own translations\",\n \"label\": \"Custom Translations\",\n \"name\": \"Translation\",\n \"type\": \"doctype\"\n }\n]" - } - ], - "category": "Administration", - "charts": [], - "creation": "2020-03-02 15:15:03.839594", - "developer_mode_only": 0, - "disable_user_customization": 0, - "docstatus": 0, - "doctype": "Desk Page", - "extends_another_page": 0, - "idx": 0, - "is_standard": 1, - "label": "Customization", - "modified": "2020-04-01 11:24:40.787109", - "modified_by": "Administrator", - "module": "Custom", - "name": "Customization", - "owner": "Administrator", - "pin_to_bottom": 0, - "pin_to_top": 0, - "shortcuts": [ - { - "label": "Customize Form", - "link_to": "Customize Form", - "type": "DocType" - }, - { - "label": "Custom Role", - "link_to": "Custom Role", - "type": "DocType" - }, - { - "label": "Custom Script", - "link_to": "Custom Script", - "type": "DocType" - } - ] -} \ No newline at end of file diff --git a/frappe/custom/doctype/custom_script/README.md b/frappe/custom/doctype/client_script/README.md similarity index 100% rename from frappe/custom/doctype/custom_script/README.md rename to frappe/custom/doctype/client_script/README.md diff --git a/frappe/custom/doctype/custom_script/__init__.py b/frappe/custom/doctype/client_script/__init__.py similarity index 100% rename from frappe/custom/doctype/custom_script/__init__.py rename to frappe/custom/doctype/client_script/__init__.py diff --git a/frappe/custom/doctype/custom_script/custom_script.js b/frappe/custom/doctype/client_script/client_script.js similarity index 95% rename from frappe/custom/doctype/custom_script/custom_script.js rename to frappe/custom/doctype/client_script/client_script.js index dc449c506a..21e7334b82 100644 --- a/frappe/custom/doctype/custom_script/custom_script.js +++ b/frappe/custom/doctype/client_script/client_script.js @@ -1,7 +1,7 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Custom Script', { +frappe.ui.form.on('Client Script', { refresh(frm) { if (frm.doc.dt && frm.doc.script) { frm.add_custom_button(__('Go to {0}', [frm.doc.dt]), @@ -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/custom_script/custom_script.json b/frappe/custom/doctype/client_script/client_script.json similarity index 86% rename from frappe/custom/doctype/custom_script/custom_script.json rename to frappe/custom/doctype/client_script/client_script.json index 328b247c49..57e6c68094 100644 --- a/frappe/custom/doctype/custom_script/custom_script.json +++ b/frappe/custom/doctype/client_script/client_script.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "creation": "2013-01-10 16:34:01", - "description": "Adds a client custom script to a DocType", + "description": "Adds a custom client script to a DocType", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -22,9 +22,7 @@ "oldfieldname": "dt", "oldfieldtype": "Link", "options": "DocType", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "script", @@ -32,35 +30,29 @@ "label": "Script", "oldfieldname": "script", "oldfieldtype": "Code", - "options": "JS", - "show_days": 1, - "show_seconds": 1 + "options": "JS" }, { "fieldname": "sample", "fieldtype": "HTML", "label": "Sample", - "options": "

      Custom Script Help

      \n

      Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started

      \n
      \n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field,  source_fieldname,  target_fieldname); \ncur_frm.add_fetch('customer',  'local_tax_no',  'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task',  'validate',  function(frm) {\n    if (frm.doc.from_date < get_today()) {\n        msgprint('You can not select past date in From Date');\n        validated = false;\n    } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task',  {\n    refresh: function(frm) {\n        // use the __islocal value of doc,  to check if the doc is saved or not\n        frm.set_df_property('myfield',  'read_only',  frm.doc.__islocal ? 0 : 1);\n    } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task',  {\n    validate: function(frm) {\n        if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n            msgprint('You are only allowed Material Receipt');\n            validated = false;\n        }\n    } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice',  {\n    validate: function(frm) {\n        // calculate incentives for each person on the deal\n        total_incentive = 0\n        $.each(frm.doc.sales_team,  function(i,  d) {\n            // calculate incentive\n            var incentive_percent = 2;\n            if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n            // actual incentive\n            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n            total_incentive += flt(d.incentives)\n        });\n        frm.doc.total_incentive = total_incentive;\n    } \n})\n\n
      ", - "show_days": 1, - "show_seconds": 1 + "options": "

      Client Script Help

      \n

      Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started

      \n
      \n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field,  source_fieldname,  target_fieldname); \ncur_frm.add_fetch('customer',  'local_tax_no',  'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task',  'validate',  function(frm) {\n    if (frm.doc.from_date < get_today()) {\n        msgprint('You can not select past date in From Date');\n        validated = false;\n    } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task',  {\n    refresh: function(frm) {\n        // use the __islocal value of doc,  to check if the doc is saved or not\n        frm.set_df_property('myfield',  'read_only',  frm.doc.__islocal ? 0 : 1);\n    } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task',  {\n    validate: function(frm) {\n        if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n            msgprint('You are only allowed Material Receipt');\n            validated = false;\n        }\n    } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice',  {\n    validate: function(frm) {\n        // calculate incentives for each person on the deal\n        total_incentive = 0\n        $.each(frm.doc.sales_team,  function(i,  d) {\n            // calculate incentive\n            var incentive_percent = 2;\n            if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n            // actual incentive\n            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n            total_incentive += flt(d.incentives)\n        });\n        frm.doc.total_incentive = total_incentive;\n    } \n})\n\n
      " }, { "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "label": "Enabled", - "show_days": 1, - "show_seconds": 1 + "label": "Enabled" } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-24 21:56:07.719579", + "modified": "2021-02-04 13:57:56.509437", "modified_by": "Administrator", "module": "Custom", - "name": "Custom Script", + "name": "Client Script", "owner": "Administrator", "permissions": [ { @@ -86,6 +78,7 @@ "write": 1 } ], + "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/custom_script/custom_script.py b/frappe/custom/doctype/client_script/client_script.py similarity index 84% rename from frappe/custom/doctype/custom_script/custom_script.py rename to frappe/custom/doctype/client_script/client_script.py index e15819de65..e252e2a750 100644 --- a/frappe/custom/doctype/custom_script/custom_script.py +++ b/frappe/custom/doctype/client_script/client_script.py @@ -5,9 +5,9 @@ import frappe from frappe.model.document import Document -class CustomScript(Document): +class ClientScript(Document): def autoname(self): - self.name = self.dt + "-Client" + self.name = self.dt def on_update(self): frappe.clear_cache(doctype=self.dt) diff --git a/frappe/custom/doctype/custom_script/test_custom_script.py b/frappe/custom/doctype/client_script/test_client_script.py similarity index 65% rename from frappe/custom/doctype/custom_script/test_custom_script.py rename to frappe/custom/doctype/client_script/test_client_script.py index 6947e6060d..de113c1ce7 100644 --- a/frappe/custom/doctype/custom_script/test_custom_script.py +++ b/frappe/custom/doctype/client_script/test_client_script.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest -# test_records = frappe.get_test_records('Custom Script') +# test_records = frappe.get_test_records('Client Script') -class TestCustomScript(unittest.TestCase): +class TestClientScript(unittest.TestCase): pass 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/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 46a2f2f9df..f5e0371c1f 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -234,7 +234,7 @@ class TestCustomizeForm(unittest.TestCase): testdt1.delete() def test_custom_action(self): - test_route = '#List/DocType' + test_route = '/app/List/DocType' # create a dummy action (route) d = self.get_customize_form("Event") diff --git a/frappe/core/page/workspace/__init__.py b/frappe/custom/doctype/doctype_layout/__init__.py similarity index 100% rename from frappe/core/page/workspace/__init__.py rename to frappe/custom/doctype/doctype_layout/__init__.py diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js new file mode 100644 index 0000000000..679330e065 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js @@ -0,0 +1,30 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('DocType Layout', { + refresh: function(frm) { + frm.trigger('document_type'); + frm.events.set_button(frm); + }, + + document_type(frm) { + frm.set_fields_as_options('fields', frm.doc.document_type, null, [], 'fieldname').then(() => { + // child table empty? then show all fields as default + if (frm.doc.document_type) { + if (!(frm.doc.fields || []).length) { + for (let f of frappe.get_doc('DocType', frm.doc.document_type).fields) { + frm.add_child('fields', { fieldname: f.fieldname, label: f.label }); + } + } + } + }); + }, + + set_button(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => { + window.open(`/app/list/${frappe.router.slug(frm.doc.name)}/list`); + }); + } + } +}); diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json new file mode 100644 index 0000000000..e47c9e03e0 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2020-11-16 17:05:35.306846", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "route", + "fields", + "client_script" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "options": "DocType Layout Field", + "reqd": 1 + }, + { + "fieldname": "client_script", + "fieldtype": "Code", + "label": "Client Script" + }, + { + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-10 15:01:04.352184", + "modified_by": "Administrator", + "module": "Custom", + "name": "DocType Layout", + "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": "Guest" + } + ], + "route": "doctype-layout", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py new file mode 100644 index 0000000000..a4fe9a9bce --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -0,0 +1,14 @@ +# -*- 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 + +from frappe.desk.utils import slug + +class DocTypeLayout(Document): + def validate(self): + if not self.route: + self.route = slug(self.name) diff --git a/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py new file mode 100644 index 0000000000..4e44743b48 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py @@ -0,0 +1,13 @@ +import frappe + +def execute(): + for web_form_name in frappe.db.get_all('Web Form', pluck='name'): + web_form = frappe.get_doc('Web Form', web_form_name) + doctype_layout = frappe.get_doc(dict( + doctype = 'DocType Layout', + document_type = web_form.doc_type, + name = web_form.title, + route = web_form.route, + fields = [dict(fieldname = d.fieldname, label=d.label) for d in web_form.web_form_fields if d.fieldname] + )).insert() + print(doctype_layout.name) \ No newline at end of file diff --git a/frappe/desk/doctype/desk_card/test_desk_card.py b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py similarity index 80% rename from frappe/desk/doctype/desk_card/test_desk_card.py rename to frappe/custom/doctype/doctype_layout/test_doctype_layout.py index de9587d17e..5765c86262 100644 --- a/frappe/desk/doctype/desk_card/test_desk_card.py +++ b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestDeskCard(unittest.TestCase): +class TestDocTypeLayout(unittest.TestCase): pass diff --git a/frappe/desk/doctype/desk_card/__init__.py b/frappe/custom/doctype/doctype_layout_field/__init__.py similarity index 100% rename from frappe/desk/doctype/desk_card/__init__.py rename to frappe/custom/doctype/doctype_layout_field/__init__.py diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json new file mode 100644 index 0000000000..a1a36216c3 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json @@ -0,0 +1,39 @@ +{ + "actions": [], + "creation": "2020-11-16 16:03:43.771801", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldname" + ], + "fields": [ + { + "fieldname": "fieldname", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldname", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-16 17:13:01.892345", + "modified_by": "Administrator", + "module": "Custom", + "name": "DocType Layout Field", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/desk_chart/desk_chart.py b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py similarity index 86% rename from frappe/desk/doctype/desk_chart/desk_chart.py rename to frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py index dbbfae6cd7..7f8c8edfce 100644 --- a/frappe/desk/doctype/desk_chart/desk_chart.py +++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class DeskChart(Document): +class DocTypeLayoutField(Document): pass 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/desk/doctype/desk_card/desk_card.py b/frappe/custom/doctype/test_rename_new/test_rename_new.py similarity index 66% rename from frappe/desk/doctype/desk_card/desk_card.py rename to frappe/custom/doctype/test_rename_new/test_rename_new.py index 01d835bbfb..aa5984e466 100644 --- a/frappe/desk/doctype/desk_card/desk_card.py +++ b/frappe/custom/doctype/test_rename_new/test_rename_new.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors +# 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 DeskCard(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/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json new file mode 100644 index 0000000000..cdc3b73366 --- /dev/null +++ b/frappe/custom/workspace/customization/customization.json @@ -0,0 +1,149 @@ +{ + "category": "Administration", + "charts": [], + "creation": "2020-03-02 15:15:03.839594", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Workspace", + "extends_another_page": 0, + "hide_custom": 0, + "icon": "customization", + "idx": 0, + "is_default": 0, + "is_standard": 1, + "label": "Customization", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Dashboards", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard", + "link_to": "Dashboard", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard Chart", + "link_to": "Dashboard Chart", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard Chart Source", + "link_to": "Dashboard Chart Source", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Form Customization", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customize Form", + "link_to": "Customize Form", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Custom Field", + "link_to": "Custom Field", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Client Script", + "link_to": "Client Script", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "DocType", + "link_to": "DocType", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Other", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Custom Translations", + "link_to": "Translation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2021-02-04 13:50:35.750463", + "modified_by": "Administrator", + "module": "Custom", + "name": "Customization", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "label": "Customize Form", + "link_to": "Customize Form", + "type": "DocType" + }, + { + "label": "Custom Role", + "link_to": "Custom Role", + "type": "DocType" + }, + { + "label": "Client Script", + "link_to": "Client Script", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Server Script", + "link_to": "Server Script", + "type": "DocType" + } + ] +} \ No newline at end of file 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 4dab313892..0ded8e0717 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -31,7 +31,7 @@ def handle_not_exist(fn): class Workspace: def __init__(self, page_name, minimal=False): self.page_name = page_name - self.extended_cards = [] + self.extended_links = [] self.extended_charts = [] self.extended_shortcuts = [] @@ -57,11 +57,11 @@ class Workspace: self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() def is_page_allowed(self): - cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module) + self.extended_cards + cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links shortcuts = self.doc.shortcuts + self.extended_shortcuts for section in cards: - links = loads(section.links) if isinstance(section.links, string_types) else section.links + links = loads(section.get('links')) if isinstance(section.get('links'), string_types) else section.get('links') for item in links: if self.is_item_allowed(item.get('name'), item.get('type')): return True @@ -108,12 +108,21 @@ class Workspace: 'extends': self.page_name, 'for_user': frappe.session.user } - pages = frappe.get_all("Desk Page", filters=filters, limit=1) - if pages: - return frappe.get_cached_doc("Desk Page", 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("Desk Page", self.page_name) + return frappe.get_cached_doc("Workspace", self.page_name) def get_onboarding_doc(self): # Check if onboarding is enabled @@ -141,25 +150,28 @@ class Workspace: return doc def get_pages_to_extend(self): - pages = frappe.get_all("Desk Page", filters={ + pages = frappe.get_all("Workspace", filters={ "extends": self.page_name, 'restrict_to_domain': ['in', frappe.get_active_domains()], 'for_user': '', 'module': ['in', self.allowed_modules] }) - pages = [frappe.get_cached_doc("Desk Page", page['name']) for page in pages] + pages = [frappe.get_cached_doc("Workspace", page['name']) for page in pages] for page in pages: - self.extended_cards = self.extended_cards + page.cards + self.extended_links = self.extended_links + page.get_link_groups() self.extended_charts = self.extended_charts + page.charts self.extended_shortcuts = self.extended_shortcuts + page.shortcuts def is_item_allowed(self, name, item_type): + if frappe.session.user == "Administrator": + return True + item_type = item_type.lower() if item_type == "doctype": - return (name in self.can_read and name in self.restricted_doctypes) + return (name in self.can_read or [] and name in self.restricted_doctypes or []) if item_type == "page": return (name in self.allowed_pages and name in self.restricted_pages) if item_type == "report": @@ -174,7 +186,7 @@ class Workspace: def build_workspace(self): self.cards = { 'label': _(self.doc.cards_label), - 'items': self.get_cards() + 'items': self.get_links() } self.charts = { @@ -196,54 +208,61 @@ class Workspace: 'items': self.get_onboarding_steps() } + def _doctype_contains_a_record(self, name): + exists = self.table_counts.get(name, False) + + if not exists and frappe.db.exists(name): + if not frappe.db.get_value('DocType', name, 'issingle'): + exists = bool(frappe.db.get_all(name, limit=1)) + else: + exists = True + self.table_counts[name] = exists + + return exists + + def _prepare_item(self, item): + if item.dependencies: + + dependencies = [dep.strip() for dep in item.dependencies.split(",")] + + incomplete_dependencies = [d for d in dependencies if not self._doctype_contains_a_record(d)] + + if len(incomplete_dependencies): + item.incomplete_dependencies = incomplete_dependencies + else: + item.incomplete_dependencies = "" + + if item.onboard: + # Mark Spotlights for initial + if item.get("type") == "doctype": + name = item.get("name") + count = self._doctype_contains_a_record(name) + + item["count"] = count + + # Translate label + item["label"] = _(item.label) if item.label else _(item.name) + + return item + @handle_not_exist - def get_cards(self): - cards = self.doc.cards + def get_links(self): + cards = self.doc.get_link_groups() + if not self.doc.hide_custom: cards = cards + get_custom_reports_and_doctypes(self.doc.module) - if len(self.extended_cards): - cards = merge_cards_based_on_label(cards + self.extended_cards) + if len(self.extended_links): + cards = merge_cards_based_on_label(cards + self.extended_links) + default_country = frappe.db.get_default("country") - def _doctype_contains_a_record(name): - exists = self.table_counts.get(name, None) - if not exists: - if not frappe.db.get_value('DocType', name, 'issingle'): - exists = frappe.db.count(name) - else: - exists = True - self.table_counts[name] = exists - return exists - - def _prepare_item(item): - if item.dependencies: - incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)] - if len(incomplete_dependencies): - item.incomplete_dependencies = incomplete_dependencies - else: - item.incomplete_dependencies = "" - - if item.onboard: - # Mark Spotlights for initial - if item.get("type") == "doctype": - name = item.get("name") - count = _doctype_contains_a_record(name) - - item["count"] = count - - # Translate label - item["label"] = _(item.label) if item.label else _(item.name) - - return item - new_data = [] - for section in cards: + for card in cards: new_items = [] - if isinstance(section.links, string_types): - links = loads(section.links) - else: - links = section.links + card = _dict(card) + + links = card.get('links', []) for item in links: item = _dict(item) @@ -253,18 +272,18 @@ class Workspace: continue # Check if user is allowed to view - if self.is_item_allowed(item.name, item.type): - prepared_item = _prepare_item(item) + if self.is_item_allowed(item.link_to, item.link_type): + prepared_item = self._prepare_item(item) new_items.append(prepared_item) if new_items: - if isinstance(section, _dict): - new_section = section.copy() + if isinstance(card, _dict): + new_card = card.copy() else: - new_section = section.as_dict().copy() - new_section["links"] = new_items - new_section["label"] = _(new_section["label"]) - new_data.append(new_section) + new_card = card.as_dict().copy() + new_card["links"] = new_items + new_card["label"] = _(new_card["label"]) + new_data.append(new_card) return new_data @@ -351,56 +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("Desk Page", fields=["name", "category", "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") @@ -427,8 +429,9 @@ def get_custom_doctype_list(module): out = [] for d in doctypes: out.append({ - "type": "doctype", - "name": d.name, + "type": "Link", + "link_type": "doctype", + "link_to": d.name, "label": _(d.name) }) @@ -444,17 +447,18 @@ def get_custom_report_list(module): out = [] for r in reports: out.append({ - "type": "report", + "type": "Link", + "link_type": "report", "doctype": r.ref_doctype, "is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0, "label": _(r.name), - "name": r.name + "link_to": r.name, }) return out def get_custom_workspace_for_user(page): - """Get custom page from desk_page if exists or create one + """Get custom page from workspace if exists or create one Args: page (stirng): Page name @@ -466,10 +470,10 @@ def get_custom_workspace_for_user(page): 'extends': page, 'for_user': frappe.session.user } - pages = frappe.get_list("Desk Page", filters=filters) + pages = frappe.get_list("Workspace", filters=filters) if pages: - return frappe.get_doc("Desk Page", pages[0]) - doc = frappe.new_doc("Desk Page") + return frappe.get_doc("Workspace", pages[0]) + doc = frappe.new_doc("Workspace") doc.extends = page doc.for_user = frappe.session.user return doc @@ -477,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 @@ -486,11 +490,12 @@ def save_customization(page, config): Returns: Boolean: Customization saving status """ - original_page = frappe.get_doc("Desk Page", page) + original_page = frappe.get_doc("Workspace", page) page_doc = get_custom_workspace_for_user(page) # Update field values page_doc.update({ + "icon": original_page.icon, "charts_label": original_page.charts_label, "cards_label": original_page.cards_label, "shortcuts_label": original_page.shortcuts_label, @@ -502,11 +507,11 @@ 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.cards = prepare_widget(config.cards, "Desk Card", "cards") + page_doc.build_links_table_from_cards(config.cards) # Set label page_doc.label = page + '-' + frappe.session.user @@ -580,15 +585,26 @@ def update_onboarding_step(name, field, value): """ frappe.db.set_value("Onboarding Step", name, field, value) +@frappe.whitelist() +def reset_customization(page): + """Reset workspace customizations for a user + + Args: + page (string): Name of the page to be reset + """ + page_doc = get_custom_workspace_for_user(page) + page_doc.delete() + def merge_cards_based_on_label(cards): """Merge cards with common label.""" cards_dict = {} for card in cards: - if card.label in cards_dict: - links = loads(cards_dict[card.label].links) + loads(card.links) - cards_dict[card.label].update(dict(links=dumps(links))) - cards_dict[card.label] = cards_dict.pop(card.label) + label = card.get('label') + if label in cards_dict: + links = loads(cards_dict[label].links) + loads(card.links) + cards_dict[label].update(dict(links=dumps(links))) + cards_dict[label] = cards_dict.pop(label) else: - cards_dict[card.label] = card + cards_dict[label] = card - return list(cards_dict.values()) \ No newline at end of file + return list(cards_dict.values()) diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 61300b920b..c640259cf2 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -3,7 +3,9 @@ frappe.ui.form.on('Dashboard', { refresh: function(frm) { - frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name)); + frm.add_custom_button(__("Show Dashboard"), + () => frappe.set_route('dashboard-view', frm.doc.name) + ); if (!frappe.boot.developer_mode && frm.doc.is_standard) { frm.disable_form(); diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index fa03bf8f80..4e66318769 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -99,7 +99,7 @@ def get_non_standard_warning_message(non_standard_docs_map): def get_html(docs, doctype): html = '

      {}

      '.format(frappe.bold(doctype)) for doc in docs: - html += ''.format(doctype=doctype, doc=doc) + html += ''.format(doctype=doctype, doc=doc) html += '
      ' return html 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/desk_card/desk_card.js b/frappe/desk/doctype/desk_card/desk_card.js deleted file mode 100644 index cc0272cb60..0000000000 --- a/frappe/desk/doctype/desk_card/desk_card.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Desk Card', { - // refresh: function(frm) { - - // } -}); diff --git a/frappe/desk/doctype/desk_card/desk_card.json b/frappe/desk/doctype/desk_card/desk_card.json deleted file mode 100644 index dbcb4b0d5c..0000000000 --- a/frappe/desk/doctype/desk_card/desk_card.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "actions": [], - "creation": "2020-01-29 14:45:54.383089", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "label", - "column_break_2", - "hidden", - "section_break_3", - "links" - ], - "fields": [ - { - "fieldname": "links", - "fieldtype": "Code", - "label": "Links", - "options": "JSON", - "reqd": 1 - }, - { - "fieldname": "section_break_3", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "hidden", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Hidden" - }, - { - "fieldname": "label", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Label", - "reqd": 1 - } - ], - "istable": 1, - "links": [], - "modified": "2020-03-31 14:38:06.303847", - "modified_by": "Administrator", - "module": "Desk", - "name": "Desk Card", - "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_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py deleted file mode 100644 index e92844ac0b..0000000000 --- a/frappe/desk/doctype/desk_page/desk_page.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- 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 frappe import _ -from frappe.utils.data import validate_json_string -from frappe.modules.export_file import export_to_files -from frappe.model.document import Document - -class DeskPage(Document): - def validate(self): - self.validate_cards_json() - 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")) - - def validate_cards_json(self): - for card in self.cards: - try: - validate_json_string(card.links) - except frappe.ValidationError: - frappe.throw(_("Invalid JSON in card links for {0}").format(frappe.bold(card.label))) - - def on_update(self): - if disable_saving_as_standard(): - return - - if frappe.conf.developer_mode and self.is_standard: - export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module) - - @staticmethod - def get_module_page_map(): - filters = { - 'extends_another_page': 0, - 'for_user': '', - } - - pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1) - - return { page[1]: page[0] for page in pages if page[1] } - -def disable_saving_as_standard(): - return frappe.flags.in_install or \ - frappe.flags.in_patch or \ - frappe.flags.in_test or \ - frappe.flags.in_fixtures or \ - frappe.flags.in_migrate \ No newline at end of file diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index f1ad41db6c..a655e9e1da 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -17,6 +17,10 @@ class KanbanBoard(Document): def on_update(self): frappe.clear_cache(doctype=self.reference_doctype) + def before_insert(self): + for column in self.columns: + column.order = get_order_for_column(self, column.column_name) + def validate_column_name(self): for column in self.columns: if not column.column_name: @@ -125,6 +129,53 @@ def update_order(board_name, order): board.save() return board, updated_cards +@frappe.whitelist() +def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index): + '''Save the order of cards in columns''' + board = frappe.get_doc('Kanban Board', board_name) + doctype = board.reference_doctype + fieldname = board.field_name + old_index = frappe.parse_json(old_index) + new_index = frappe.parse_json(new_index) + + # save current order and index of columns to be updated + from_col_order, from_col_idx = get_kanban_column_order_and_index(board, from_colname) + to_col_order, to_col_idx = get_kanban_column_order_and_index(board, to_colname) + + if from_colname == to_colname: + from_col_order = to_col_order + + to_col_order.insert(new_index, from_col_order.pop((old_index))) + + # save updated order + board.columns[from_col_idx].order = frappe.as_json(from_col_order) + board.columns[to_col_idx].order = frappe.as_json(to_col_order) + board.save() + + # update changed value in doc + frappe.set_value(doctype, docname, fieldname, to_colname) + + return board + +def get_kanban_column_order_and_index(board, colname): + for i, col in enumerate(board.columns): + if col.column_name == colname: + col_order = frappe.parse_json(col.order) + col_idx = i + + return col_order, col_idx + +@frappe.whitelist() +def add_card(board_name, docname, colname): + board = frappe.get_doc('Kanban Board', board_name) + + col_order, col_idx = get_kanban_column_order_and_index(board, colname) + col_order.insert(0, docname) + + board.columns[col_idx].order = frappe.as_json(col_order) + + board.save() + return board @frappe.whitelist() def quick_kanban_board(doctype, board_name, field_name, project=None): @@ -133,6 +184,13 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): doc = frappe.new_doc('Kanban Board') meta = frappe.get_meta(doctype) + doc.kanban_board_name = board_name + doc.reference_doctype = doctype + doc.field_name = field_name + + if project: + doc.filters = '[["Task","project","=","{0}"]]'.format(project) + options = '' for field in meta.fields: if field.fieldname == field_name: @@ -149,12 +207,6 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): column_name=column )) - doc.kanban_board_name = board_name - doc.reference_doctype = doctype - doc.field_name = field_name - - if project: - doc.filters = '[["Task","project","=","{0}"]]'.format(project) if doctype in ['Note', 'ToDo']: doc.private = 1 @@ -162,6 +214,12 @@ def quick_kanban_board(doctype, board_name, field_name, project=None): doc.save() return doc +def get_order_for_column(board, colname): + filters = [[board.reference_doctype, board.field_name, '=', colname]] + if board.filters: + filters.append(frappe.parse_json(board.filters)[0]) + + return frappe.as_json(frappe.get_list(board.reference_doctype, filters=filters, pluck='name')) @frappe.whitelist() def update_column_order(board_name, order): diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py index 89160a60f0..8315c0b304 100644 --- a/frappe/desk/doctype/module_onboarding/module_onboarding.py +++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py @@ -41,3 +41,15 @@ class ModuleOnboarding(Document): def before_export(self, doc): doc.is_complete = 0 + + def reset_onboarding(self): + frappe.only_for("Administrator") + + self.is_complete = 0 + steps = self.get_steps() + for step in steps: + step.is_complete = 0 + step.is_skipped = 0 + step.save() + + self.save() diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py index 64a68312a5..c54689418e 100644 --- a/frappe/desk/doctype/note/note.py +++ b/frappe/desk/doctype/note/note.py @@ -17,7 +17,7 @@ class Note(Document): # expire this notification in a week (default) self.expire_notification_on = frappe.utils.add_days(self.creation, 7) - def before_print(self): + def before_print(self, settings=None): self.print_heading = self.name self.sub_heading = "" diff --git a/frappe/desk/doctype/note/note_list.js b/frappe/desk/doctype/note/note_list.js index c2758ce53b..f7f8d37dcf 100644 --- a/frappe/desk/doctype/note/note_list.js +++ b/frappe/desk/doctype/note/note_list.js @@ -7,7 +7,7 @@ frappe.listview_settings['Note'] = { if(doc.public) { return [__("Public"), "green", "public,=,Yes"]; } else { - return [__("Private"), "darkgrey", "public,=,No"]; + return [__("Private"), "gray", "public,=,No"]; } } } diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 9b124cd6f4..34726bdf8a 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -42,7 +42,6 @@ def create_notification_settings(user): _doc = frappe.new_doc('Notification Settings') _doc.name = user _doc.insert(ignore_permissions=True) - frappe.db.commit() @frappe.whitelist() 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/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json index 79d659b1ed..f71e821f65 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.json +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json @@ -8,14 +8,18 @@ "field_order": [ "title", "column_break_2", - "is_mandatory", "is_complete", "is_skipped", + "description_section", + "description", + "intro_video_url", "section_break_5", "action", + "action_label", "column_break_7", "reference_document", "show_full_form", + "show_form_tour", "is_single", "reference_report", "report_reference_doctype", @@ -30,13 +34,6 @@ "video_url" ], "fields": [ - { - "default": "0", - "fieldname": "is_mandatory", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Is Mandatory" - }, { "default": "0", "fieldname": "is_complete", @@ -181,10 +178,38 @@ "fieldname": "show_full_form", "fieldtype": "Check", "label": "Show Full Form?" + }, + { + "description": "Description to inform the user about any action that is going to be performed", + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "description", + "fieldtype": "Markdown Editor", + "label": "Description" + }, + { + "fieldname": "intro_video_url", + "fieldtype": "Data", + "label": "Intro Video URL" + }, + { + "fieldname": "action_label", + "fieldtype": "Data", + "label": "Action Label" + }, + { + "default": "0", + "depends_on": "eval:doc.action==\"Create Entry\" && doc.show_full_form", + "fieldname": "show_form_tour", + "fieldtype": "Check", + "label": "Show Form Tour" } ], "links": [], - "modified": "2020-08-06 12:55:20.377679", + "modified": "2020-10-30 14:54:06.646513", "modified_by": "Administrator", "module": "Desk", "name": "Onboarding Step", diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index 8086acbb2a..e1cc5dfba4 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -10,7 +10,3 @@ class OnboardingStep(Document): def before_export(self, doc): doc.is_complete = 0 doc.is_skipped = 0 - - def validate(self): - if self.action == "Go to Page": - self.is_mandatory = 0 diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index af695a861a..7e016ee91b 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -25,6 +25,17 @@ def add_tag(tag, dt, dn, color=None): return tag +@frappe.whitelist() +def add_tags(tags, dt, docs, color=None): + "adds a new tag to a record, and creates the Tag master" + tags = frappe.parse_json(tags) + docs = frappe.parse_json(docs) + for doc in docs: + for tag in tags: + DocTags(dt).add(doc, tag) + + # return tag + @frappe.whitelist() def remove_tag(tag, dt, dn): "removes tag from the record" diff --git a/frappe/desk/doctype/desk_page/__init__.py b/frappe/desk/doctype/workspace/__init__.py similarity index 100% rename from frappe/desk/doctype/desk_page/__init__.py rename to frappe/desk/doctype/workspace/__init__.py diff --git a/frappe/desk/doctype/desk_page/test_desk_page.py b/frappe/desk/doctype/workspace/test_workspace.py similarity index 81% rename from frappe/desk/doctype/desk_page/test_desk_page.py rename to frappe/desk/doctype/workspace/test_workspace.py index 7553364744..7a3f122ee2 100644 --- a/frappe/desk/doctype/desk_page/test_desk_page.py +++ b/frappe/desk/doctype/workspace/test_workspace.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestDeskPage(unittest.TestCase): +class TestWorkspace(unittest.TestCase): pass diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/workspace/workspace.js similarity index 84% rename from frappe/desk/doctype/desk_page/desk_page.js rename to frappe/desk/doctype/workspace/workspace.js index 503859eb61..19d429f9f6 100644 --- a/frappe/desk/doctype/desk_page/desk_page.js +++ b/frappe/desk/doctype/workspace/workspace.js @@ -1,11 +1,14 @@ // Copyright (c) 2020, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Desk Page', { +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/desk_page/desk_page.json b/frappe/desk/doctype/workspace/workspace.json similarity index 86% rename from frappe/desk/doctype/desk_page/desk_page.json rename to frappe/desk/doctype/workspace/workspace.json index 2b8aea5e6c..fff766a3bf 100644 --- a/frappe/desk/doctype/desk_page/desk_page.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -12,10 +12,12 @@ "extends", "module", "category", + "icon", "restrict_to_domain", "onboarding", "column_break_3", "extends_another_page", + "is_default", "is_standard", "developer_mode_only", "disable_user_customization", @@ -30,7 +32,7 @@ "shortcuts", "section_break_18", "cards_label", - "cards" + "links" ], "fields": [ { @@ -41,6 +43,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "charts", "fieldname": "section_break_2", "fieldtype": "Section Break", "label": "Dashboards" @@ -49,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", + "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", @@ -78,12 +81,6 @@ "fieldname": "column_break_3", "fieldtype": "Column Break" }, - { - "fieldname": "cards", - "fieldtype": "Table", - "label": "Cards", - "options": "Desk Card" - }, { "fieldname": "category", "fieldtype": "Select", @@ -144,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" @@ -176,7 +175,7 @@ "fieldtype": "Link", "in_standard_filter": 1, "label": "Extends", - "options": "Desk Page", + "options": "Workspace", "search_index": 1 }, { @@ -197,13 +196,32 @@ "fieldname": "hide_custom", "fieldtype": "Check", "label": "Hide Custom DocTypes and Reports" - } + }, + { + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "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-05-18 19:17:27.206646", + "modified": "2021-01-21 12:09:36.156614", "modified_by": "Administrator", "module": "Desk", - "name": "Desk Page", + "name": "Workspace", "owner": "Administrator", "permissions": [ { diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py new file mode 100644 index 0000000000..0934138821 --- /dev/null +++ b/frappe/desk/doctype/workspace/workspace.py @@ -0,0 +1,127 @@ +# -*- 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 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 + +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(): + return + + if frappe.conf.developer_mode and self.is_standard: + export_to_files(record_list=[['Workspace', self.name]], record_module=self.module) + + @staticmethod + def get_module_page_map(): + filters = { + 'extends_another_page': 0, + 'for_user': '', + } + + pages = frappe.get_all("Workspace", fields=["name", "module"], filters=filters, as_list=1) + + return { page[1]: page[0] for page in pages if page[1] } + + def get_link_groups(self): + cards = [] + current_card = { + "label": "Link", + "type": "Card Break", + "icon": None, + "hidden": False, + } + + card_links = [] + + for link in self.links: + link = link.as_dict() + if link.type == "Card Break": + + if card_links: + current_card['links'] = card_links + cards.append(current_card) + + current_card = link + card_links = [] + else: + card_links.append(link) + + current_card['links'] = card_links + cards.append(current_card) + + return cards + + def build_links_table_from_cards(self, config): + # Empty links table + self.links = [] + order = config.get('order') + widgets = config.get('widgets') + + for idx, name in enumerate(order): + card = widgets[name].copy() + links = loads(card.get('links')) + + self.append('links', { + "label": card.get('label'), + "type": "Card Break", + "icon": card.get('icon'), + "hidden": card.get('hidden') or False + }) + + for link in links: + self.append('links', { + "label": link.get('label'), + "type": "Link", + "link_type": link.get('link_type'), + "link_to": link.get('link_to'), + "onboard": link.get('onboard'), + "only_for": link.get('only_for'), + "dependencies": link.get('dependencies'), + "is_query_report": link.get('is_query_report') + }) + + +def disable_saving_as_standard(): + return frappe.flags.in_install or \ + frappe.flags.in_patch or \ + frappe.flags.in_test or \ + frappe.flags.in_fixtures or \ + frappe.flags.in_migrate + +def get_link_type(key): + key = key.lower() + + link_type_map = { + "doctype": "DocType", + "page": "Page", + "report": "Report" + } + + if key in link_type_map: + return link_type_map[key] + + return "DocType" + +def get_report_type(report): + report_type = frappe.get_value("Report", report, "report_type") + return report_type in ["Query Report", "Script Report", "Custom Report"] diff --git a/frappe/desk/doctype/desk_shortcut/__init__.py b/frappe/desk/doctype/workspace_chart/__init__.py similarity index 100% rename from frappe/desk/doctype/desk_shortcut/__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/public/css/variables.css b/frappe/desk/doctype/workspace_link/__init__.py similarity index 100% rename from frappe/public/css/variables.css rename to frappe/desk/doctype/workspace_link/__init__.py diff --git a/frappe/desk/doctype/workspace_link/workspace_link.json b/frappe/desk/doctype/workspace_link/workspace_link.json new file mode 100644 index 0000000000..010fb3f316 --- /dev/null +++ b/frappe/desk/doctype/workspace_link/workspace_link.json @@ -0,0 +1,117 @@ +{ + "actions": [], + "creation": "2020-11-16 15:30:45.784417", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "label", + "icon", + "hidden", + "link_details_section", + "link_type", + "link_to", + "column_break_7", + "dependencies", + "only_for", + "onboard", + "is_query_report" + ], + "fields": [ + { + "default": "Link", + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "Link\nCard Break", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "depends_on": "eval:doc.type == \"Card Break\"", + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Card Break\"", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "depends_on": "eval:doc.type == \"Link\"", + "fieldname": "link_details_section", + "fieldtype": "Section Break", + "label": "Link Details" + }, + { + "fieldname": "link_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Link Type", + "mandatory_depends_on": "eval:doc.type==\"Link\"", + "options": "DocType\nPage\nReport", + "read_only_depends_on": "eval:doc.type!=\"Link\"" + }, + { + "fieldname": "link_to", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link To", + "mandatory_depends_on": "eval:doc.type==\"Link\"", + "options": "link_type", + "read_only_depends_on": "eval:doc.type!=\"Link\"" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "dependencies", + "fieldtype": "Data", + "label": "Dependencies" + }, + { + "fieldname": "only_for", + "fieldtype": "Link", + "label": "Only for ", + "options": "Country" + }, + { + "default": "0", + "fieldname": "onboard", + "fieldtype": "Check", + "label": "Onboard" + }, + { + "default": "0", + "depends_on": "eval:doc.link_type == \"Report\"", + "fieldname": "is_query_report", + "fieldtype": "Check", + "label": "Is Query Report" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-12 13:13:12.379443", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Link", + "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/desk/doctype/workspace_link/workspace_link.py similarity index 66% rename from frappe/desk/doctype/desk_shortcut/desk_shortcut.py rename to frappe/desk/doctype/workspace_link/workspace_link.py index bbf0b2e074..8a139077a6 100644 --- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.py +++ b/frappe/desk/doctype/workspace_link/workspace_link.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors +# 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 DeskShortcut(Document): +class WorkspaceLink(Document): pass diff --git a/frappe/public/website_theme/.gitkeep b/frappe/desk/doctype/workspace_shortcut/__init__.py similarity index 100% rename from frappe/public/website_theme/.gitkeep rename to frappe/desk/doctype/workspace_shortcut/__init__.py diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json similarity index 96% rename from frappe/desk/doctype/desk_shortcut/desk_shortcut.json rename to frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json index fbcc2dfb82..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,55 +84,25 @@ "fieldname": "column_break_3", "fieldtype": "Column Break" }, - { - "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": "Color", "label": "Color" }, { - "depends_on": "eval:frappe.boot.developer_mode", - "fieldname": "icon", + "description": "For example: {} Open", + "fieldname": "format", "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 + "label": "Format" } ], "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/document_follow.py b/frappe/desk/form/document_follow.py index 66164948f2..f5ace4d732 100644 --- a/frappe/desk/form/document_follow.py +++ b/frappe/desk/form/document_follow.py @@ -10,7 +10,15 @@ from frappe import _ from itertools import groupby @frappe.whitelist() -def follow_document(doctype, doc_name, user, force=False): +def update_follow(doctype, doc_name, following): + if following: + return follow_document(doctype, doc_name, frappe.session.user) + else: + return unfollow_document(doctype, doc_name, frappe.session.user) + + +@frappe.whitelist() +def follow_document(doctype, doc_name, user): ''' param: Doctype name @@ -76,7 +84,6 @@ def send_email_alert(receiver, docinfo, timeline): ) def send_document_follow_mails(frequency): - ''' param: frequency for sanding mails diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index cacbd3c633..1f5c437330 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -13,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed from frappe import _ from six.moves.urllib.parse import quote -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def getdoc(doctype, name, user=None): """ Loads a doclist for a given document. This method is called directly from the client. @@ -52,7 +52,7 @@ def getdoc(doctype, name, user=None): frappe.response.docs.append(doc) -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def getdoctype(doctype, with_parent=False, cached_timestamp=None): """load doctype""" @@ -91,13 +91,17 @@ def get_docinfo(doc=None, doctype=None, name=None): raise frappe.PermissionError frappe.response["docinfo"] = { "attachments": get_attachments(doc.doctype, doc.name), + "attachment_logs": get_comments(doc.doctype, doc.name, 'attachment'), "communications": _get_communications(doc.doctype, doc.name), 'comments': get_comments(doc.doctype, doc.name), 'total_comments': len(json.loads(doc.get('_comments') or '[]')), 'versions': get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), + "assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'), "permissions": get_doc_permissions(doc), "shared": frappe.share.get_users(doc.doctype, doc.name), + "share_logs": get_comments(doc.doctype, doc.name, 'share'), + "like_logs": get_comments(doc.doctype, doc.name, 'Like'), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), @@ -128,15 +132,27 @@ def get_communications(doctype, name, start=0, limit=20): return _get_communications(doctype, name, start, limit) -def get_comments(doctype, name): - comments = frappe.get_all('Comment', fields = ['*'], filters = dict( +def get_comments(doctype, name, comment_type='Comment'): + comment_types = [comment_type] + + if comment_type == 'share': + comment_types = ['Shared', 'Unshared'] + + elif comment_type == 'assignment': + comment_types = ['Assignment Completed', 'Assigned'] + + elif comment_type == 'attachment': + comment_types = ['Attachment', 'Attachment Removed'] + + comments = frappe.get_all('Comment', fields = ['name', 'creation', 'content', 'owner', 'comment_type'], filters=dict( reference_doctype = doctype, - reference_name = name + reference_name = name, + comment_type = ['in', comment_types] )) # convert to markdown (legacy ?) - for c in comments: - if c.comment_type == 'Comment': + if comment_type == 'Comment': + for c in comments: c.content = frappe.utils.markdown(c.content) return comments @@ -222,13 +238,12 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= def get_assignments(dt, dn): cl = frappe.get_all("ToDo", - fields=['name', 'owner', 'description', 'status'], - limit= 5, - filters={ - 'reference_type': dt, - 'reference_name': dn, - 'status': ('!=', 'Cancelled'), - }) + fields=['name', 'owner', 'description', 'status'], + filters={ + 'reference_type': dt, + 'reference_name': dn, + 'status': ('!=', 'Cancelled'), + }) return cl diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index c28a40657f..c63da93a33 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -130,7 +130,7 @@ class FormMeta(Meta): def add_custom_script(self): """embed all require files""" # custom script - custom = frappe.db.get_value("Custom Script", {"dt": self.name, "enabled": 1}, "script") or "" + custom = frappe.db.get_value("Client Script", {"dt": self.name, "enabled": 1}, "script") or "" self.set("__custom_js", custom) @@ -202,13 +202,17 @@ class FormMeta(Meta): self.load_kanban_column_fields() def load_kanban_column_fields(self): - values = frappe.get_list( - 'Kanban Board', fields=['field_name'], - filters={'reference_doctype': self.name}) + try: + values = frappe.get_list( + 'Kanban Board', fields=['field_name'], + filters={'reference_doctype': self.name}) - fields = [x['field_name'] for x in values] - fields = list(set(fields)) - self.set("__kanban_column_fields", fields, as_value=True) + fields = [x['field_name'] for x in values] + fields = list(set(fields)) + self.set("__kanban_column_fields", fields, as_value=True) + except frappe.PermissionError: + # no access to kanban board + pass def get_code_files_via_hooks(hook, name): code_files = [] diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 4c3bab2e23..395d2b9571 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -47,7 +47,7 @@ def validate_link(): except Exception as e: error_message = str(e).split("Unknown column '") fieldname = None if len(error_message)<=1 else error_message[1].split("'")[0] - frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom script").format(fieldname)) + frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom client script").format(fieldname)) frappe.errprint(frappe.get_traceback()) if fetch_value: diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index e5654c853f..8d00ea9bc2 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -8,7 +8,8 @@ def get_leaderboards(): 'User': { 'fields': ['points'], 'method': 'frappe.desk.leaderboard.get_energy_point_leaderboard', - 'company_disabled': 1 + 'company_disabled': 1, + 'icon': 'users' } } return leaderboards diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index 1d10a13930..91dc0f3ba9 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def get_list_settings(doctype): try: return frappe.get_cached_doc("List View Settings", doctype) diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index ad696520f8..df25b77e2d 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -553,4 +553,4 @@ def get_report_list(module, is_standard="No"): "name": r.name }) - return out + return out \ No newline at end of file diff --git a/frappe/desk/page/activity/activity.css b/frappe/desk/page/activity/activity.css index 87604ff7a5..b2387135c7 100644 --- a/frappe/desk/page/activity/activity.css +++ b/frappe/desk/page/activity/activity.css @@ -10,38 +10,42 @@ cursor: pointer; } +#page-activity hr { + border-top: 1px solid var(--dark-border-color); +} + .activity-label { max-width: 100px; margin-bottom: -4px; } .date-indicator { - background:none; - font-size:12px; - vertical-align:middle; - font-weight:bold; - color:#6c7680; + background: none; + font-size: 12px; + vertical-align: middle; + font-weight: bold; + color: var(--text-muted); } .date-indicator::after { - margin:0 -4px 0 12px; - content:''; - display:inline-block; - height:8px; - width:8px; - border-radius:8px; - background: #d1d8dd; + margin: 0 -4px 0 12px; + content: ""; + display: inline-block; + height: 8px; + width: 8px; + border-radius: 8px; + background: var(--dark-border-color); } .date-indicator.blue { - color: #5e64ff; + color: var(--primary); } .date-indicator.blue::after { - background: #5e64ff; + background: var(--primary); } .activity-message { - border-left: 1px solid #d1d8dd; + border-left: 1px solid var(--dark-border-color); padding: 15px; padding-right: 30px; } @@ -57,14 +61,14 @@ } #page-activity .octicon-heart { - color: #ff5858; + color: var(--red-500); margin: 0px 5px; } .heatmap { - padding-top: 30px; + padding-top: 30px; } .heatmap svg { margin: auto; -} \ No newline at end of file +} diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js index 4ec54e1b2d..39de414122 100644 --- a/frappe/desk/page/activity/activity.js +++ b/frappe/desk/page/activity/activity.js @@ -3,7 +3,7 @@ frappe.provide("frappe.activity"); -frappe.pages['activity'].on_page_load = function(wrapper) { +frappe.pages['activity'].on_page_load = function (wrapper) { var me = this; frappe.ui.make_app_page({ @@ -14,7 +14,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { me.page = wrapper.page; me.page.set_title(__("Activity")); - frappe.model.with_doctype("Communication", function() { + frappe.model.with_doctype("Communication", function () { me.page.list = new frappe.views.Activity({ doctype: 'Communication', parent: wrapper @@ -23,7 +23,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { frappe.activity.render_heatmap(me.page); - me.page.main.on("click", ".activity-message", function() { + me.page.main.on("click", ".activity-message", function () { var link_doctype = $(this).attr("data-link-doctype"), link_name = $(this).attr("data-link-name"), doctype = $(this).attr("data-doctype"), @@ -47,13 +47,13 @@ frappe.pages['activity'].on_page_load = function(wrapper) { }); // Build Report Button - if(frappe.boot.user.can_get_report.indexOf("Feed")!=-1) { - this.page.add_menu_item(__('Build Report'), function() { + if (frappe.boot.user.can_get_report.indexOf("Feed") != -1) { + this.page.add_menu_item(__('Build Report'), function () { frappe.set_route("List", "Feed", "Report"); }, 'fa fa-th') } - this.page.add_menu_item(__('Activity Log'), function() { + this.page.add_menu_item(__('Activity Log'), function () { frappe.route_options = { "user": frappe.session.user } @@ -62,43 +62,43 @@ frappe.pages['activity'].on_page_load = function(wrapper) { }, 'fa fa-th'); }; -frappe.pages['activity'].on_page_show = function() { +frappe.pages['activity'].on_page_show = function () { frappe.breadcrumbs.add("Desk"); } frappe.activity.last_feed_date = false; frappe.activity.Feed = Class.extend({ - init: function(row, data) { + init: function (row, data) { this.scrub_data(data); this.add_date_separator(row, data); - if(!data.add_class) + if (!data.add_class) data.add_class = "label-default"; data.link = ""; if (data.link_doctype && data.link_name) { - data.link = frappe.format(data.link_name, {fieldtype: "Link", options: data.link_doctype}, - {label: __(data.link_doctype) + " " + __(data.link_name)}); + data.link = frappe.format(data.link_name, { fieldtype: "Link", options: data.link_doctype }, + { label: __(data.link_doctype) + " " + __(data.link_name) }); - } else if (data.feed_type==="Comment" && data.comment_type==="Comment") { + } else if (data.feed_type === "Comment" && data.comment_type === "Comment") { // hack for backward compatiblity data.link_doctype = data.reference_doctype; data.link_name = data.reference_name; data.reference_doctype = "Communication"; data.reference_name = data.name; - data.link = frappe.format(data.link_name, {fieldtype: "Link", options: data.link_doctype}, - {label: __(data.link_doctype) + " " + __(data.link_name)}); + data.link = frappe.format(data.link_name, { fieldtype: "Link", options: data.link_doctype }, + { label: __(data.link_doctype) + " " + __(data.link_name) }); } else if (data.reference_doctype && data.reference_name) { - data.link = frappe.format(data.reference_name, {fieldtype: "Link", options: data.reference_doctype}, - {label: __(data.reference_doctype) + " " + __(data.reference_name)}); + data.link = frappe.format(data.reference_name, { fieldtype: "Link", options: data.reference_doctype }, + { label: __(data.reference_doctype) + " " + __(data.reference_name) }); } $(row) .append(frappe.render_template("activity_row", data)) .find("a").addClass("grey"); }, - scrub_data: function(data) { + scrub_data: function (data) { data.by = frappe.user.full_name(data.owner); data.avatar = frappe.avatar(data.owner); @@ -114,22 +114,23 @@ frappe.activity.Feed = Class.extend({ data.when = comment_when(data.creation); data.feed_type = data.comment_type || data.communication_medium; }, - add_date_separator: function(row, data) { + + add_date_separator: function (row, data) { var date = frappe.datetime.str_to_obj(data.creation); var last = frappe.activity.last_feed_date; - if((last && frappe.datetime.obj_to_str(last) != frappe.datetime.obj_to_str(date)) || (!last)) { + if ((last && frappe.datetime.obj_to_str(last) != frappe.datetime.obj_to_str(date)) || (!last)) { var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); var pdate; - if(diff < 1) { + if (diff < 1) { pdate = 'Today'; - } else if(diff < 2) { + } else if (diff < 2) { pdate = 'Yesterday'; } else { pdate = frappe.datetime.global_date_format(date); } data.date_sep = pdate; - data.date_class = pdate=='Today' ? "date-indicator blue" : "date-indicator"; + data.date_class = pdate == 'Today' ? "date-indicator blue" : "date-indicator"; } else { data.date_sep = null; data.date_class = ""; @@ -138,29 +139,29 @@ frappe.activity.Feed = Class.extend({ } }); -frappe.activity.render_heatmap = function(page) { - var me = this; +frappe.activity.render_heatmap = function (page) { $('
      \
      \
      ').prependTo(page.main); frappe.call({ method: "frappe.desk.page.activity.activity.get_heatmap_data", - callback: function(r) { - if(r.message) { - var heatmap = new frappe.Chart(".heatmap", { + callback: function (r) { + if (r.message) { + new frappe.Chart(".heatmap", { type: 'heatmap', start: new Date(moment().subtract(1, 'year').toDate()), countLabel: "actions", - discreteDomains: 0, + discreteDomains: 1, + radius: 3, // default 0 data: { 'dataPoints': r.message } }); } } - }) -} + }); +}; frappe.views.Activity = class Activity extends frappe.views.BaseList { constructor(opts) { @@ -181,6 +182,10 @@ frappe.views.Activity = class Activity extends frappe.views.BaseList { // } + setup_view_menu() { + // + } + setup_sort_selector() { } diff --git a/frappe/desk/page/backups/backups.css b/frappe/desk/page/backups/backups.css new file mode 100644 index 0000000000..13f093e0b1 --- /dev/null +++ b/frappe/desk/page/backups/backups.css @@ -0,0 +1,13 @@ +.download-backups { + font-size: var(--text-base); +} + +.download-backup-card { + display: block; + text-decoration: none; +} + +.download-backup-card:hover { + box-shadow: var(--shadow-md); + text-decoration: none; +} diff --git a/frappe/desk/page/backups/backups.html b/frappe/desk/page/backups/backups.html index 0056b6814b..e63481487c 100644 --- a/frappe/desk/page/backups/backups.html +++ b/frappe/desk/page/backups/backups.html @@ -1,32 +1,20 @@ - - - - - - - - - - - {% for f in files %} - - - - - - {% endfor %} - -
      - {{ _("Date") }} - - {{ _("File") }} - - {{ _("Size") }} -
      - {{ f[1] }} - - {{ f[0] }} - - {{ f[2] }} -
      +
      + {% for f in files %} + + {% endfor %} +
      \ No newline at end of file diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py index 386af70a4e..eaa0c65143 100644 --- a/frappe/desk/page/backups/backups.py +++ b/frappe/desk/page/backups/backups.py @@ -9,7 +9,7 @@ import datetime def get_context(context): def get_time(path): dt = os.path.getmtime(path) - return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime('%Y-%m-%d %H:%M') + return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime('%a %b %d %H:%M %Y') def get_size(path): size = os.path.getsize(path) diff --git a/frappe/desk/page/leaderboard/leaderboard.css b/frappe/desk/page/leaderboard/leaderboard.css index a3cb4d09c4..d15b4ffcd4 100644 --- a/frappe/desk/page/leaderboard/leaderboard.css +++ b/frappe/desk/page/leaderboard/leaderboard.css @@ -52,6 +52,34 @@ } .rank { - max-width: 75px; + max-width: 100px; } +.leaderboard .result { + border-top: 1px solid var(--border-color); +} + +.leaderboard .list-item { + padding-left: 45px; +} + +.leaderboard .list-item_content { + padding-right: 60px; +} + +.leaderboard-sidebar { + padding-left: 0; + position: fixed; +} + +.leaderboard-list { + padding: var(-padding-sm) 0; + min-height: 70vh; +} + +.leaderboard-empty-state { + align-items: center; + height: 70vh; + justify-content: center; + display: flex; +} diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 189949ac68..b3fccf84f9 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -14,11 +14,13 @@ class Leaderboard { frappe.ui.make_app_page({ parent: parent, title: __("Leaderboard"), - single_column: false + single_column: false, + card_layout: true, }); + this.parent = parent; this.page = this.parent.page; - this.page.sidebar.html(``); + this.page.sidebar.html(`
        `); this.$sidebar_list = this.page.sidebar.find('ul'); this.get_leaderboard_config(); @@ -79,7 +81,8 @@ class Leaderboard { this.$graph_area = this.$container.find(".leaderboard-graph"); this.doctypes.map(doctype => { - this.get_sidebar_item(doctype).appendTo(this.$sidebar_list); + const icon = this.leaderboard_config[doctype].icon; + this.get_sidebar_item(doctype, icon).appendTo(this.$sidebar_list); }); this.setup_leaderboard_fields(); @@ -138,16 +141,16 @@ class Leaderboard { } create_date_range_field() { - let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`); + let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`); this.date_range_field = $(`
        `).insertAfter(timespan_field).hide(); let date_field = frappe.ui.form.make_control({ 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-sm', + input_class: 'input-xs', reqd: 1, change: () => { this.selected_date_range = date_field.get_value(); @@ -163,7 +166,7 @@ class Leaderboard { this.$sidebar_list.on("click", "li", (e)=> { let $li = $(e.currentTarget); - let doctype = $li.find("span").attr("doctype-value"); + let doctype = $li.find(".doctype-text").attr("doctype-value"); this.options.selected_company = frappe.defaults.get_default("company"); this.options.selected_doctype = doctype; @@ -181,8 +184,8 @@ class Leaderboard { $(this.parent).find("[data-original-title=Company]").show(); } - this.$sidebar_list.find("li").removeClass("active"); - $li.addClass("active"); + this.$sidebar_list.find("li").removeClass("active selected"); + $li.addClass("active selected"); frappe.set_route("leaderboard", this.options.selected_doctype); this.make_request(); @@ -193,7 +196,7 @@ class Leaderboard { this.$search_box = $(``); $(this.parent).find(".page-form").append(this.$search_box); @@ -236,21 +239,16 @@ class Leaderboard { let graph_items = results.slice(0, 10); this.$graph_area.show().empty(); - let args = { + + const custom_options = { data: { - datasets: [ - { - values: graph_items.map(d => d.value) - } - ], + datasets: [{ values: graph_items.map(d => d.value) }], labels: graph_items.map(d => d.name) }, - colors: ["light-green"], format_tooltip_x: d => d[this.options.selected_filter_item], - type: "bar", height: 140 }; - new frappe.Chart(".leaderboard-graph", args); + frappe.utils.make_chart('.leaderboard-graph', custom_options); notify(this, r); }); @@ -263,7 +261,7 @@ class Leaderboard { frappe.utils.setup_search($(me.parent), ".list-item-container", ".list-id"); } else { me.$graph_area.hide(); - me.message = __("No items found."); + me.message = __("No Items Found"); me.$container.find(".leaderboard-list").html(me.render_list_view()); } } @@ -292,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 ( `
        -
        ${filters}
        +
        ${filters}
        `; return html; } @@ -330,14 +328,16 @@ class Leaderboard { } render_message() { - - let html = - `
        -
        -

        No Item found

        -
        -
        `; - + const display_class = this.message ? '' : 'hide'; + let html = `
        +
        + Empty State +
        ${this.message}
        +
        +
        `; return html; } @@ -348,13 +348,13 @@ class Leaderboard { return fieldname === this.options.selected_filter_item; })); - const link = `#Form/${this.options.selected_doctype}/${item.name}`; + const link = `/app/${frappe.router.slug(this.options.selected_doctype)}/${item.name}`; const name_html = item.formatted_name ? `${item.formatted_name}` : ` ${item.name} `; const html = `
        -
        +
        ${index}
        @@ -368,10 +368,13 @@ class Leaderboard { return html; } - get_sidebar_item(item) { - return $(`
      • - - ${ __(item) } + get_sidebar_item(item, icon) { + let icon_html = icon ? frappe.utils.icon(icon, 'md') : ''; + return $(`
      • + ${icon_html} + + ${ __(item) } +
      • `); } diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index 60e1f3242a..6d3aaee22b 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -18,14 +18,14 @@ def install(): @frappe.whitelist() def update_genders(): - default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")] + default_genders = ["Male", "Female", "Other","Transgender", "Genderqueer", "Non-Conforming","Prefer not to say"] records = [{'doctype': 'Gender', 'gender': d} for d in default_genders] for record in records: frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) @frappe.whitelist() def update_salutations(): - default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")] + default_salutations = ["Mr", "Ms", 'Mx', "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"] records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] for record in records: doc = frappe.new_doc(record.get("doctype")) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 9a950a694d..f44a57e339 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -9,36 +9,34 @@ frappe.setup = { utils: {}, domains: [], - on: function(event, fn) { - if(!frappe.setup.events[event]) { + on: function (event, fn) { + if (!frappe.setup.events[event]) { frappe.setup.events[event] = []; } frappe.setup.events[event].push(fn); }, - add_slide: function(slide) { + add_slide: function (slide) { frappe.setup.slides.push(slide); }, - remove_slide: function(slide_name){ + remove_slide: function (slide_name) { frappe.setup.slides = frappe.setup.slides.filter((slide) => slide.name !== slide_name); }, - run_event: function(event) { - $.each(frappe.setup.events[event] || [], function(i, fn) { + run_event: function (event) { + $.each(frappe.setup.events[event] || [], function (i, fn) { fn(); }); } } -frappe.pages['setup-wizard'].on_page_load = function(wrapper) { +frappe.pages['setup-wizard'].on_page_load = function (wrapper) { let requires = (frappe.boot.setup_wizard_requires || []); - - - frappe.require(requires, function() { + frappe.require(requires, function () { frappe.call({ method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages", freeze: true, - callback: function(r) { + callback: function (r) { frappe.setup.data.lang = r.message; frappe.setup.run_event("before_load"); @@ -47,12 +45,13 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) { slides: frappe.setup.slides, slide_class: frappe.setup.SetupWizardSlide, unidirectional: 1, + done_state: 1, before_load: ($footer) => { $footer.find('.next-btn').removeClass('btn-default') .addClass('btn-primary'); $footer.find('.text-right').prepend( - $(` - ${__("Complete Setup")}`)); + $(``)); } } @@ -60,7 +59,7 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) { frappe.setup.run_event("after_load"); // frappe.wizard.values = test_values_edu; let route = frappe.get_route(); - if(route) { + if (route) { frappe.wizard.show_slide(route[1]); } } @@ -68,16 +67,16 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) { }); }; -frappe.pages['setup-wizard'].on_page_show = function(wrapper) { - if(frappe.get_route()[1]) { +frappe.pages['setup-wizard'].on_page_show = function () { + if (frappe.get_route()[1]) { frappe.wizard && frappe.wizard.show_slide(frappe.get_route()[1]); } }; -frappe.setup.on("before_load", function() { +frappe.setup.on("before_load", function () { // load slides frappe.setup.slides_settings.forEach((s) => { - if(!(s.name==='user' && frappe.boot.developer_mode)) { + if (!(s.name === 'user' && frappe.boot.developer_mode)) { // if not user slide with developer mode frappe.setup.add_slide(s); } @@ -89,8 +88,8 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { super(args); $.extend(this, args); - this.welcomed = true; this.page_name = "setup-wizard"; + this.welcomed = true; frappe.set_route("setup-wizard/0"); } @@ -113,7 +112,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { handle_enter_press(e) { if (e.which === frappe.ui.keyCode.ENTER) { var $target = $(e.target); - if($target.hasClass('prev-btn')) { + if ($target.hasClass('prev-btn')) { $target.trigger('click'); } else { this.container.find('.next-btn').trigger('click'); @@ -123,7 +122,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } before_show_slide() { - if(!this.welcomed) { + if (!this.welcomed) { frappe.set_route(this.page_name); return false; } @@ -142,10 +141,10 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { show_hide_prev_next(id) { super.show_hide_prev_next(id); - if (id + 1 === this.slides.length){ + if (id + 1 === this.slides.length) { this.$next_btn.removeClass("btn-primary").hide(); this.$complete_btn.addClass("btn-primary").show() - .on('click', this.action_on_complete.bind(this)); + .on('click', () => this.action_on_complete()); } else { this.$next_btn.addClass("btn-primary").show(); @@ -155,7 +154,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { refresh_slides() { // For Translations, etc. - if(this.in_refresh_slides || !this.current_slide.set_values()) { + if (this.in_refresh_slides || !this.current_slide.set_values()) { return; } this.in_refresh_slides = true; @@ -171,7 +170,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { // re-render all slide, only remake made slides $.each(this.slide_dict, (id, slide) => { - if(slide.made) { + if (slide.made) { this.made_slide_ids.push(id); } }); @@ -194,30 +193,30 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { return frappe.call({ method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete", - args: {args: this.values}, + args: { args: this.values }, callback: (r) => { - if(r.message.status === 'ok') { + if (r.message.status === 'ok') { this.post_setup_success(); - } else if(r.message.fail !== undefined) { + } else if (r.message.fail !== undefined) { this.abort_setup(r.message.fail); } }, - error: this.abort_setup.bind(this, "Error in setup", true) + error: () => this.abort_setup("Error in setup") }); } post_setup_success() { this.set_setup_complete_message(__("Setup Complete"), __("Refreshing...")); - if(frappe.setup.welcome_page) { + if (frappe.setup.welcome_page) { localStorage.setItem("session_last_route", frappe.setup.welcome_page); } - setTimeout(function() { + setTimeout(function () { // Reload - window.location.href = '/desk'; + window.location.href = '/app'; }, 2000); } - abort_setup(fail_msg, error=false) { + abort_setup(fail_msg) { this.$working_state.find('.state-icon-container').html(''); fail_msg = fail_msg ? fail_msg : __("Failed to complete setup"); @@ -231,12 +230,12 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { listen_for_setup_stages() { frappe.realtime.on("setup_task", (data) => { // console.log('data', data); - if(data.stage_status) { + if (data.stage_status) { // .html('Process '+ data.progress[0] + ' of ' + data.progress[1] + ': ' + data.stage_status); this.update_setup_message(data.stage_status); - this.set_setup_load_percent((data.progress[0]+1)/data.progress[1] * 100); + this.set_setup_load_percent((data.progress[0] + 1) / data.progress[1] * 100); } - if(data.fail_msg) { + if (data.fail_msg) { this.abort_setup(data.fail_msg); } }) @@ -248,8 +247,8 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { get_setup_slides_filtered_by_domain() { var filtered_slides = []; - frappe.setup.slides.forEach(function(slide) { - if(frappe.setup.domains) { + frappe.setup.slides.forEach(function (slide) { + if (frappe.setup.domains) { let active_domains = frappe.setup.domains; if (!slide.domains || slide.domains.filter(d => active_domains.includes(d)).length > 0) { @@ -277,8 +276,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } attach_abort_button() { - this.$abort_btn = $(``); + this.$abort_btn = $(``); this.$working_state.find('.content').append(this.$abort_btn); this.$abort_btn.on('click', () => { @@ -290,18 +288,18 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { this.$abort_btn.hide(); } - get_message(title, message="") { - const loading_html = `
        -
        -
        + get_message(title, message = "") { + const loading_html = `
        +
        +
        `; - return $(`
        + return $(`
        -

        ${title}

        +

        ${title}

        ${loading_html}
        -

        ${message}

        +

        ${message}

        `); } @@ -312,7 +310,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } set_setup_load_percent(percent) { - this.$working_state.find('.progress-bar').css({"width": percent + "%"}); + this.$working_state.find('.progress-bar').css({ "width": percent + "%" }); } }; @@ -327,13 +325,13 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { this.reset_action_button_state(); } - set_init_values () { + set_init_values() { var me = this; // set values from frappe.setup.values - if(frappe.wizard.values && this.fields) { - this.fields.forEach(function(f) { + if (frappe.wizard.values && this.fields) { + this.fields.forEach(function (f) { var value = frappe.wizard.values[f.fieldname]; - if(value) { + if (value) { me.get_field(f.fieldname).set_input(value); } }); @@ -354,11 +352,13 @@ frappe.setup.slides_settings = [ // help: __("Let's prepare the system for first use."), fields: [ - { fieldname: "language", label: __("Your Language"), - fieldtype: "Select", reqd: 1} + { + fieldname: "language", label: __("Your Language"), + fieldtype: "Select", reqd: 1 + } ], - onload: function(slide) { + onload: function (slide) { this.setup_fields(slide); var language_field = slide.get_field("language"); @@ -372,7 +372,7 @@ frappe.setup.slides_settings = [ moment.locale("en"); }, - setup_fields: function(slide) { + setup_fields: function (slide) { frappe.setup.utils.setup_language_field(slide); frappe.setup.utils.bind_language_events(slide); }, @@ -385,25 +385,31 @@ frappe.setup.slides_settings = [ icon: "fa fa-flag", // help: __("Select your Country, Time Zone and Currency"), fields: [ - { fieldname: "country", label: __("Your Country"), reqd:1, - fieldtype: "Select" }, + { + fieldname: "country", label: __("Your Country"), reqd: 1, + fieldtype: "Select" + }, { fieldtype: "Section Break" }, - { fieldname: "timezone", label: __("Time Zone"), reqd:1, - fieldtype: "Select" }, + { + fieldname: "timezone", label: __("Time Zone"), reqd: 1, + fieldtype: "Select" + }, { fieldtype: "Column Break" }, - { fieldname: "currency", label: __("Currency"), reqd:1, - fieldtype: "Select" } + { + fieldname: "currency", label: __("Currency"), reqd: 1, + fieldtype: "Select" + } ], - onload: function(slide) { - if(frappe.setup.data.regional_data) { + onload: function (slide) { + if (frappe.setup.data.regional_data) { this.setup_fields(slide); } else { frappe.setup.utils.load_regional_data(slide, this.setup_fields); } }, - setup_fields: function(slide) { + setup_fields: function (slide) { frappe.setup.utils.setup_region_fields(slide); frappe.setup.utils.bind_region_events(slide); } @@ -415,24 +421,30 @@ frappe.setup.slides_settings = [ title: __("The First User: You"), icon: "fa fa-user", fields: [ - { "fieldtype":"Attach Image", "fieldname":"attach_user_image", - label: __("Attach Your Picture"), is_private: 0, align: 'center'}, - { "fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", - reqd:1}, - { "fieldname": "email", "label": __("Email Address") + ' (' + __("Will be your login ID") + ')', - "fieldtype": "Data", "options":"Email"}, + { + "fieldtype": "Attach Image", "fieldname": "attach_user_image", + label: __("Attach Your Picture"), is_private: 0, align: 'center' + }, + { + "fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", + reqd: 1 + }, + { + "fieldname": "email", "label": __("Email Address") + ' (' + __("Will be your login ID") + ')', + "fieldtype": "Data", "options": "Email" + }, { "fieldname": "password", "label": __("Password"), "fieldtype": "Password" } ], // help: __('The first user will become the System Manager (you can change this later).'), - onload: function(slide) { - if(frappe.session.user!=="Administrator") { + onload: function (slide) { + if (frappe.session.user !== "Administrator") { slide.form.fields_dict.email.$wrapper.toggle(false); slide.form.fields_dict.password.$wrapper.toggle(false); // remove password field delete slide.form.fields_dict.password; - if(frappe.boot.user.first_name || frappe.boot.user.last_name) { + if (frappe.boot.user.first_name || frappe.boot.user.last_name) { slide.form.fields_dict.full_name.set_input( [frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim()); } @@ -440,7 +452,7 @@ frappe.setup.slides_settings = [ var user_image = frappe.get_cookie("user_image"); var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; - if(user_image) { + if (user_image) { $attach_user_image.find(".missing-image").toggle(false); $attach_user_image.find("img").attr("src", decodeURIComponent(user_image)); $attach_user_image.find(".img-container").toggle(true); @@ -457,11 +469,11 @@ frappe.setup.slides_settings = [ } }, - setup_fields: function(slide) { - if(frappe.setup.data.full_name) { + setup_fields: function (slide) { + if (frappe.setup.data.full_name) { slide.form.fields_dict.full_name.set_input(frappe.setup.data.full_name); } - if(frappe.setup.data.email) { + if (frappe.setup.data.email) { let email = frappe.setup.data.email; slide.form.fields_dict.email.set_input(email); if (frappe.get_gravatar(email, 200)) { @@ -476,21 +488,21 @@ frappe.setup.slides_settings = [ ]; frappe.setup.utils = { - load_regional_data: function(slide, callback) { + load_regional_data: function (slide, callback) { frappe.call({ - method:"frappe.geo.country_info.get_country_timezone_info", - callback: function(data) { + method: "frappe.geo.country_info.get_country_timezone_info", + callback: function (data) { frappe.setup.data.regional_data = data.message; callback(slide); } }); }, - load_user_details: function(slide, callback) { + load_user_details: function (slide, callback) { frappe.call({ method: "frappe.desk.page.setup_wizard.setup_wizard.load_user_details", freeze: true, - callback: function(r) { + callback: function (r) { frappe.setup.data.full_name = r.message.full_name; frappe.setup.data.email = r.message.email; callback(slide); @@ -498,13 +510,13 @@ frappe.setup.utils = { }) }, - setup_language_field: function(slide) { + setup_language_field: function (slide) { var language_field = slide.get_field("language"); language_field.df.options = frappe.setup.data.lang.languages; language_field.refresh(); }, - setup_region_fields: function(slide) { + setup_region_fields: function (slide) { /* Set a slide's country, timezone and currency fields */ @@ -516,33 +528,34 @@ frappe.setup.utils = { .add_options([""].concat(Object.keys(data.country_info).sort())); slide.get_input("currency").empty() - .add_options(frappe.utils.unique([""].concat($.map(data.country_info, - function(opts, country) { return opts.currency; }))).sort()); + .add_options(frappe.utils.unique([""].concat( + $.map(data.country_info, opts => opts.currency) + )).sort()); slide.get_input("timezone").empty() .add_options([""].concat(data.all_timezones)); // set values if present - if(frappe.wizard.values.country) { + if (frappe.wizard.values.country) { country_field.set_input(frappe.wizard.values.country); } else if (data.default_country) { country_field.set_input(data.default_country); } - if(frappe.wizard.values.currency) { + if (frappe.wizard.values.currency) { slide.get_field("currency").set_input(frappe.wizard.values.currency); } - if(frappe.wizard.values.timezone) { + if (frappe.wizard.values.timezone) { slide.get_field("timezone").set_input(frappe.wizard.values.timezone); } }, - bind_language_events: function(slide) { - slide.get_input("language").unbind("change").on("change", function() { - clearTimeout (slide.language_call_timeout); - slide.language_call_timeout = setTimeout (() => { + bind_language_events: function (slide) { + slide.get_input("language").unbind("change").on("change", function () { + clearTimeout(slide.language_call_timeout); + slide.language_call_timeout = setTimeout(() => { var lang = $(this).val() || "English"; frappe._messages = {}; frappe.call({ @@ -551,7 +564,7 @@ frappe.setup.utils = { args: { language: lang }, - callback: function(r) { + callback: function () { frappe.setup._from_load_messages = true; frappe.wizard.refresh_slides(); } @@ -560,11 +573,11 @@ frappe.setup.utils = { }); }, - bind_region_events: function(slide) { + bind_region_events: function (slide) { /* Bind a slide's country, timezone and currency fields */ - slide.get_input("country").on("change", function() { + slide.get_input("country").on("change", function () { var country = slide.get_input("country").val(); var $timezone = slide.get_input("timezone"); var data = frappe.setup.data.regional_data; @@ -572,7 +585,7 @@ frappe.setup.utils = { $timezone.empty(); // add country specific timezones first - if(country) { + if (country) { var timezone_list = data.country_info[country].timezones || []; $timezone.add_options(timezone_list.sort()); slide.get_field("currency").set_input(data.country_info[country].currency); @@ -589,16 +602,16 @@ frappe.setup.utils = { || "dd-mm-yyyy"); }); - slide.get_input("currency").on("change", function() { + slide.get_input("currency").on("change", function () { var currency = slide.get_input("currency").val(); if (!currency) return; - frappe.model.with_doc("Currency", currency, function() { + frappe.model.with_doc("Currency", currency, function () { frappe.provide("locals.:Currency." + currency); var currency_doc = frappe.model.get_doc("Currency", currency); var number_format = currency_doc.number_format; - if (number_format==="#.###") { + if (number_format === "#.###") { number_format = "#.###,##"; - } else if (number_format==="#,###") { + } else if (number_format === "#,###") { number_format = "#,###.##" } diff --git a/frappe/desk/page/translation_tool/translation_tool.css b/frappe/desk/page/translation_tool/translation_tool.css index 52d34777dd..9603a4ce35 100644 --- a/frappe/desk/page/translation_tool/translation_tool.css +++ b/frappe/desk/page/translation_tool/translation_tool.css @@ -5,6 +5,7 @@ cursor: pointer; overflow: hidden; } + .translation-item:hover { background-color: #fafbfc; } @@ -19,8 +20,9 @@ } .translation-tool { - border: 0px 1px 1px 1px solid #d1d8dd; + display: flex; width: 100%; + padding: 0; height: 72vh; } diff --git a/frappe/desk/page/translation_tool/translation_tool.js b/frappe/desk/page/translation_tool/translation_tool.js index 892bab32ce..b3f0c032e3 100644 --- a/frappe/desk/page/translation_tool/translation_tool.js +++ b/frappe/desk/page/translation_tool/translation_tool.js @@ -2,7 +2,8 @@ frappe.pages['translation-tool'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, title: 'Translation Tool', - single_column: true + single_column: true, + card_layout: true, }); frappe.translation_tool = new TranslationTool(page); @@ -250,7 +251,7 @@ class TranslationTool { set_status(translation) { this.form.get_field('header').$wrapper.find('.translation-status').html(` - + ${this.get_indicator_status_text(translation)} `); @@ -347,7 +348,7 @@ class TranslationTool { ) .then(() => { frappe.dom.unfreeze(); - frappe.show_alert(__('Successfully Submitted!')); + frappe.show_alert({ message: __('Successfully Submitted!'), indicator: 'success'}); this.edited_translations = {}; this.update_header(); this.fetch_messages_then_render(); @@ -426,10 +427,13 @@ class TranslationTool { let edited_translations_count = Object.keys(this.edited_translations) .length; if (edited_translations_count) { - this.page.set_indicator( - __('{0} translations pending', [edited_translations_count]), - 'orange' - ); + let message = ''; + if (edited_translations_count == 1) { + message = __('{0} translation pending', [edited_translations_count]); + } else { + message = __('{0} translations pending', [edited_translations_count]); + } + this.page.set_indicator(message, 'orange'); } else { this.page.set_indicator(''); } diff --git a/frappe/desk/page/user_profile/user_profile.css b/frappe/desk/page/user_profile/user_profile.css index c05a52ada2..9bcfc3394a 100644 --- a/frappe/desk/page/user_profile/user_profile.css +++ b/frappe/desk/page/user_profile/user_profile.css @@ -1,121 +1,30 @@ -.user-image-container { - margin-top: 7px; - padding-bottom: 100%; +.recent-activity .new-timeline { + padding-top: 0; } -.user-image-container .standard-image { - font-size: 72px; +.recent-activity .new-timeline:before { + top: 25px; } -.profile-details { - margin: -5px 5px; +.recent-activity-title { + font-weight: 700; + font-size: var(--text-xl); + color: var(--text-color); } -.profile-links { - margin: 30px 5px; +.recent-activity .recent-activity-footer { + margin-left: calc(var(--timeline-left-padding) + var(--timeline-item-left-margin)); + max-width: var(--timeline-content-max-width); } -.user-initial { - font-size: 72px; -} - -.chart-column-container{ - border-bottom: 1px solid #d1d8dd; - margin: 5px 0; -} - -.chart-container text.title { - text-transform: uppercase; - font-size: 11px; -} - -.heatmap-container { - height: 170px -} - -.performance-heatmap { - width: 80%; - display: inline-block; -} - -.performance-heatmap .chart-container { - margin-left: 30px; -} - -.performance-heatmap .frappe-chart { - margin-top: 5px; -} - -.performance-heatmap .frappe-chart .chart-legend { - display: none; -} - -.performance-percentage-chart .frappe-chart { - position: absolute; - top: 5px; -} - -.performance-line-chart .frappe-chart { - margin-top: -20px; -} - -.percentage-chart-container { - height: 130px; -} - -.line-chart-container .chart-filter { - z-index: 1; +.recent-activity .show-more-activity-btn { + display: block; + margin: auto; + width: max-content; + margin-top: 35px; + font-size: var(--text-md); } .recent-activity { - margin: 20px; - font-size: 12px; -} - -.show-more-activity { - text-align: center; - margin-top: 20px; -} - -.recent-activity-item { - margin: 15px 5px; -} - -.interest-icon { - margin-right: 5px; -} - -.chart-filter { - position: relative; - top: 5px; - margin-right: 10px; -} - -.filter-label { - margin-right: 4px; -} - -.performance-title { - position: relative; - left: 30px; - top: 20px; -} - -@media (max-width: 991px) { - .user-profile-sidebar { - display: flex; - } - - .percentage-chart-container { - border-top: 1px solid #d1d8dd; - } - - .user-profile-sidebar .profile-links { - margin: 0; - } - - .user-profile-sidebar .profile-details { - width: 50%; - margin: 0; - } -} + padding-bottom: 60px; +} \ No newline at end of file diff --git a/frappe/desk/page/user_profile/user_profile.html b/frappe/desk/page/user_profile/user_profile.html index ca9c8f0001..911ccc702d 100644 --- a/frappe/desk/page/user_profile/user_profile.html +++ b/frappe/desk/page/user_profile/user_profile.html @@ -1,25 +1,44 @@ - +
        +
        + +
        +
        +

        {%=__("Type Distribution") %}

        +
        +
        +
        +
        + No Data to Show +
        +
        +
        +
        +
        +

        {%=__("Energy Points") %}

        +
        +
        +
        +
        + No Data to Show +
        +
        +
        +
        +
        +
        {%=__("Recent Activity") %}
        +
        + +
        +
        +
        \ No newline at end of file diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index 5f91b376e8..3443a33942 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -1,414 +1,6 @@ -frappe.provide('frappe.energy_points'); - -frappe.pages['user-profile'].on_page_load = function(wrapper) { - - frappe.ui.make_app_page({ - parent: wrapper, - title: __('User Profile'), - }); - - let user_profile = new UserProfile(wrapper); - $(wrapper).bind('show', ()=> { +frappe.pages['user-profile'].on_page_load = function (wrapper) { + frappe.require('assets/js/user_profile_controller.min.js', () => { + let user_profile = new frappe.ui.UserProfile(wrapper); user_profile.show(); }); -}; - -class UserProfile { - - constructor(wrapper) { - this.wrapper = $(wrapper); - this.page = wrapper.page; - this.sidebar = this.wrapper.find('.layout-side-section'); - this.main_section = this.wrapper.find('.layout-main-section'); - } - - show() { - let route = frappe.get_route(); - this.user_id = route[1] || frappe.session.user; - - //validate if user - if (route.length > 1) { - frappe.db.exists('User', this.user_id).then( exists => { - if (exists) { - this.make_user_profile(); - } else { - frappe.msgprint(__('User does not exist')); - } - }); - } else { - this.user_id = frappe.session.user; - this.make_user_profile(); - } - } - - make_user_profile() { - frappe.set_route('user-profile', this.user_id, { redirect: true }); - this.user = frappe.user_info(this.user_id); - this.page.set_title(this.user.fullname); - this.setup_user_search(); - this.main_section.empty().append(frappe.render_template('user_profile')); - this.energy_points = 0; - this.review_points = 0; - this.rank = 0; - this.month_rank = 0; - this.render_user_details(); - this.render_points_and_rank(); - this.render_heatmap(); - this.render_line_chart(); - this.render_percentage_chart('type', 'Type Distribution'); - this.create_percentage_chart_filters(); - this.setup_show_more_activity(); - this.render_user_activity(); - } - - setup_user_search() { - this.$user_search_button = this.page.set_secondary_action(__('Change User'), () => { - this.show_user_search_dialog(); - }); - } - - show_user_search_dialog() { - let dialog = new frappe.ui.Dialog({ - title: __('Change User'), - fields: [ - { - fieldtype: 'Link', - fieldname: 'user', - options: 'User', - label: __('User'), - } - ], - primary_action_label: __('Go'), - primary_action: ({ user }) => { - dialog.hide(); - this.user_id = user; - this.make_user_profile(); - } - }); - dialog.show(); - } - - render_heatmap() { - this.heatmap = new frappe.Chart('.performance-heatmap', { - type: 'heatmap', - countLabel: 'Energy Points', - data: {}, - discreteDomains: 0, - }); - this.update_heatmap_data(); - this.create_heatmap_chart_filters(); - } - - update_heatmap_data(date_from) { - frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_heatmap_data', { - user: this.user_id, - date: date_from || frappe.datetime.year_start(), - }).then((r) => { - this.heatmap.update( {dataPoints: r} ); - }); - } - - - render_line_chart() { - this.line_chart_filters = [ - ['Energy Point Log', 'user', '=', this.user_id, false], - ['Energy Point Log', 'type', '!=', 'Review', false] - ]; - - this.line_chart_config = { - timespan: 'Last Month', - time_interval: 'Daily', - type: 'Line', - value_based_on: 'points', - chart_type: 'Sum', - document_type: 'Energy Point Log', - name: 'Energy Points', - width: 'half', - based_on: 'creation' - }; - - this.line_chart = new frappe.Chart( '.performance-line-chart', { - title: 'Energy Points', - type: 'line', - height: 200, - data: { - labels: [], - datasets: [{}] - }, - colors: ['purple'], - axisOptions: { - xIsSeries: 1 - } - }); - this.update_line_chart_data(); - this.create_line_chart_filters(); - } - - update_line_chart_data() { - this.line_chart_config.filters_json = JSON.stringify(this.line_chart_filters); - - frappe.xcall('frappe.desk.doctype.dashboard_chart.dashboard_chart.get', { - chart: this.line_chart_config, - no_cache: 1, - }).then(chart => { - this.line_chart.update(chart); - }); - } - - render_percentage_chart(field, title) { - 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', { - title: title, - type: 'percentage', - data: { - labels: chart.labels, - datasets: chart.datasets - }, - truncateLegends: 1, - barOptions: { - height: 11, - depth: 1 - }, - height: 160, - maxSlices: 8, - colors: ['#5e64ff', '#743ee2', '#ff5858', '#ffa00a', '#feef72', '#28a745', '#98d85b', '#a9a7ac'], - }); - } else { - this.wrapper.find('.percentage-chart-container').hide(); - } - }); - } - - create_line_chart_filters() { - let filters = [ - { - label: 'All', - options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'], - action: (selected_item) => { - if (selected_item === 'All') { - this.line_chart_filters = [ - ['Energy Point Log', 'user', '=', this.user_id, false], - ['Energy Point Log', 'type', '!=', 'Review', false] - ]; - } else { - this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false]; - } - this.update_line_chart_data(); - } - }, - { - label: 'Last Month', - options: ['Last Week', 'Last Month', 'Last Quarter', 'Last Year'], - action: (selected_item) => { - this.line_chart_config.timespan = selected_item; - this.update_line_chart_data(); - } - }, - { - label: 'Daily', - options: ['Daily', 'Weekly', 'Monthly'], - action: (selected_item) => { - this.line_chart_config.time_interval = selected_item; - this.update_line_chart_data(); - } - }, - ]; - frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.line-chart-container', 1); - } - - create_percentage_chart_filters() { - let filters = [ - { - label: 'Type', - options: ['Type', 'Reference Doctype', 'Rule'], - fieldnames: ['type', 'reference_doctype', 'rule'], - action: (selected_item, fieldname) => { - let title = selected_item + ' Distribution'; - this.render_percentage_chart(fieldname, title); - } - }, - ]; - frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.percentage-chart-container'); - } - - create_heatmap_chart_filters() { - let filters = [ - { - label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()), - options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation), - action: (selected_item) => { - this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item)); - } - }, - ]; - frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.heatmap-container'); - } - - - edit_profile() { - let edit_profile_dialog = new frappe.ui.Dialog({ - title: __('Edit Profile'), - fields: [ - { - fieldtype: 'Attach Image', - fieldname: 'user_image', - label: 'Profile Image', - }, - { - fieldtype: 'Data', - fieldname: 'interest', - label: 'Interests', - }, - { - fieldtype: 'Column Break' - }, - { - fieldtype: 'Data', - fieldname: 'location', - label: 'Location', - }, - { - fieldtype: 'Section Break', - fieldname: 'Interest', - }, - { - fieldtype: 'Small Text', - fieldname: 'bio', - label: 'Bio', - } - ], - primary_action: values => { - edit_profile_dialog.disable_primary_action(); - frappe.xcall('frappe.desk.page.user_profile.user_profile.update_profile_info', { - profile_info: values - }).then(user => { - user.image = user.user_image; - this.user = Object.assign(values, user); - edit_profile_dialog.hide(); - this.render_user_details(); - }).finally(() => { - edit_profile_dialog.enable_primary_action(); - }); - }, - primary_action_label: __('Save') - }); - - edit_profile_dialog.set_values({ - user_image: this.user.image, - location: this.user.location, - interest: this.user.interest, - bio: this.user.bio - }); - edit_profile_dialog.show(); - } - - render_user_details() { - this.sidebar.empty().append(frappe.render_template('user_profile_sidebar', { - user_image: frappe.avatar(this.user_id, 'avatar-frame', 'user_image', this.user.image), - user_abbr: this.user.abbr, - user_location: this.user.location, - user_interest: this.user.interest, - user_bio: this.user.bio, - })); - - this.setup_user_profile_links(); - } - - setup_user_profile_links() { - if (this.user_id !== frappe.session.user) { - this.wrapper.find('.profile-links').hide(); - } else { - this.wrapper.find('.edit-profile-link').on('click', () => { - this.edit_profile(); - }); - - this.wrapper.find('.user-settings-link').on('click', () => { - this.go_to_user_settings(); - }); - } - } - - get_user_rank() { - return frappe.xcall('frappe.desk.page.user_profile.user_profile.get_user_rank', { - user: this.user_id, - }).then(r => { - if (r.monthly_rank.length) this.month_rank = r.monthly_rank[0]; - if (r.all_time_rank.length) this.rank = r.all_time_rank[0]; - }); - } - - get_user_points() { - return frappe.xcall( - 'frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points', - { - user: this.user_id, - } - ).then(r => { - if (r[this.user_id]) { - this.energy_points = r[this.user_id].energy_points; - this.review_points = r[this.user_id].review_points; - } - }); - } - - render_points_and_rank() { - let $profile_details = this.wrapper.find('.profile-details'); - - this.get_user_rank().then(() => { - this.get_user_points().then(() => { - let html = $(` -

        ${__('Energy Points:')} ${this.energy_points}

        -

        ${__('Review Points:')} ${this.review_points}

        -

        ${__('Rank:')} ${this.rank}

        -

        ${__('Monthly Rank:')} ${this.month_rank}

        - `); - - $profile_details.append(html); - }); - }); - } - - go_to_user_settings() { - frappe.set_route('Form', 'User', this.user_id); - } - - render_user_activity() { - this.$recent_activity_list = this.wrapper.find('.recent-activity-list'); - - let get_recent_energy_points_html = (field) => { - let message_html = frappe.energy_points.format_history_log(field); - return `

        ${message_html}

        `; - }; - - frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_list', { - start: this.activity_start, - limit: this.activity_end, - user: this.user_id - }).then(list => { - if (list.length < 11) { - let activity_html = `${__('No More Activity')}`; - this.wrapper.find('.show-more-activity').html(activity_html); - } - let html = list.slice(0, 10).map(get_recent_energy_points_html).join(''); - this.$recent_activity_list.append(html); - }); - } - - setup_show_more_activity() { - //Show 10 items at a time - this.activity_start = 0; - this.activity_end = 11; - this.wrapper.find('.show-more-activity').on('click', () => this.show_more_activity()); - } - - show_more_activity() { - this.activity_start = this.activity_end; - this.activity_end += 11; - this.render_user_activity(); - } - -} +}; \ No newline at end of file diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js new file mode 100644 index 0000000000..c1a89f316e --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile_controller.js @@ -0,0 +1,453 @@ +import BaseTimeline from "../../../public/js/frappe/form/footer/base_timeline"; +frappe.provide('frappe.energy_points'); + +class UserProfile { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = frappe.ui.make_app_page({ + parent: wrapper, + }); + this.sidebar = this.wrapper.find('.layout-side-section'); + this.main_section = this.wrapper.find('.layout-main-section'); + this.wrapper.bind('show', () => { + this.show(); + }); + } + + show() { + let route = frappe.get_route(); + this.user_id = route[1] || frappe.session.user; + + //validate if user + if (route.length > 1) { + frappe.dom.freeze(__('Loading user profile') + '...'); + frappe.db.exists('User', this.user_id).then(exists => { + frappe.dom.unfreeze(); + if (exists) { + this.make_user_profile(); + } else { + frappe.msgprint(__('User does not exist')); + } + }); + } else { + frappe.set_route('user-profile', frappe.session.user); + } + } + + make_user_profile() { + this.user = frappe.user_info(this.user_id); + this.page.set_title(this.user.fullname); + this.setup_user_search(); + this.main_section.empty().append(frappe.render_template('user_profile')); + this.energy_points = 0; + this.review_points = 0; + this.rank = 0; + this.month_rank = 0; + this.render_user_details(); + this.render_points_and_rank(); + this.render_heatmap(); + this.render_line_chart(); + this.render_percentage_chart('type', 'Type Distribution'); + this.create_percentage_chart_filters(); + this.setup_user_activity_timeline(); + } + + setup_user_search() { + this.$user_search_button = this.page.set_secondary_action( + __('Change User'), + () => this.show_user_search_dialog(), + { icon: 'change', size: 'sm' } + ); + } + + show_user_search_dialog() { + let dialog = new frappe.ui.Dialog({ + title: __('Change User'), + fields: [ + { + fieldtype: 'Link', + fieldname: 'user', + options: 'User', + label: __('User'), + } + ], + primary_action_label: __('Go'), + primary_action: ({ user }) => { + dialog.hide(); + this.user_id = user; + this.make_user_profile(); + } + }); + dialog.show(); + } + + render_heatmap() { + this.heatmap = new frappe.Chart('.performance-heatmap', { + type: 'heatmap', + countLabel: 'Energy Points', + data: {}, + discreteDomains: 1, + radius: 3, + height: 150 + }); + this.update_heatmap_data(); + this.create_heatmap_chart_filters(); + } + + update_heatmap_data(date_from) { + frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_heatmap_data', { + user: this.user_id, + date: date_from || frappe.datetime.year_start(), + }).then((r) => { + this.heatmap.update({ dataPoints: r }); + }); + } + + + render_line_chart() { + this.line_chart_filters = [ + ['Energy Point Log', 'user', '=', this.user_id, false], + ['Energy Point Log', 'type', '!=', 'Review', false] + ]; + + this.line_chart_config = { + timespan: 'Last Month', + time_interval: 'Daily', + type: 'Line', + value_based_on: 'points', + chart_type: 'Sum', + document_type: 'Energy Point Log', + name: 'Energy Points', + width: 'half', + based_on: 'creation' + }; + + this.line_chart = new frappe.Chart('.performance-line-chart', { + type: 'line', + height: 200, + data: { + labels: [], + datasets: [{}] + }, + colors: ['purple'], + axisOptions: { + xIsSeries: 1 + } + }); + this.update_line_chart_data(); + this.create_line_chart_filters(); + } + + update_line_chart_data() { + this.line_chart_config.filters_json = JSON.stringify(this.line_chart_filters); + + frappe.xcall('frappe.desk.doctype.dashboard_chart.dashboard_chart.get', { + chart: this.line_chart_config, + no_cache: 1, + }).then(chart => { + this.line_chart.update(chart); + }); + } + + // eslint-disable-next-line no-unused-vars + render_percentage_chart(field, title) { + 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() { + let filters = [ + { + label: 'All', + options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'], + action: (selected_item) => { + if (selected_item === 'All') { + this.line_chart_filters = [ + ['Energy Point Log', 'user', '=', this.user_id, false], + ['Energy Point Log', 'type', '!=', 'Review', false] + ]; + } else { + this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false]; + } + this.update_line_chart_data(); + } + }, + { + label: 'Last Month', + options: ['Last Week', 'Last Month', 'Last Quarter', 'Last Year'], + action: (selected_item) => { + this.line_chart_config.timespan = selected_item; + this.update_line_chart_data(); + } + }, + { + label: 'Daily', + options: ['Daily', 'Weekly', 'Monthly'], + action: (selected_item) => { + this.line_chart_config.time_interval = selected_item; + this.update_line_chart_data(); + } + }, + ]; + frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.line-chart-options', 1); + } + + create_percentage_chart_filters() { + let filters = [ + { + label: 'Type', + options: ['Type', 'Reference Doctype', 'Rule'], + fieldnames: ['type', 'reference_doctype', 'rule'], + action: (selected_item, fieldname) => { + let title = selected_item + ' Distribution'; + this.render_percentage_chart(fieldname, title); + } + }, + ]; + frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.percentage-chart-options'); + } + + create_heatmap_chart_filters() { + let filters = [ + { + label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()), + options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation), + action: (selected_item) => { + this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item)); + } + }, + ]; + frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.heatmap-options'); + } + + + edit_profile() { + let edit_profile_dialog = new frappe.ui.Dialog({ + title: __('Edit Profile'), + fields: [ + { + fieldtype: 'Attach Image', + fieldname: 'user_image', + label: 'Profile Image', + }, + { + fieldtype: 'Data', + fieldname: 'interest', + label: 'Interests', + }, + { + fieldtype: 'Column Break' + }, + { + fieldtype: 'Data', + fieldname: 'location', + label: 'Location', + }, + { + fieldtype: 'Section Break', + fieldname: 'Interest', + }, + { + fieldtype: 'Small Text', + fieldname: 'bio', + label: 'Bio', + } + ], + primary_action: values => { + edit_profile_dialog.disable_primary_action(); + frappe.xcall('frappe.desk.page.user_profile.user_profile.update_profile_info', { + profile_info: values + }).then(user => { + user.image = user.user_image; + this.user = Object.assign(values, user); + edit_profile_dialog.hide(); + this.render_user_details(); + }).finally(() => { + edit_profile_dialog.enable_primary_action(); + }); + }, + primary_action_label: __('Save') + }); + + edit_profile_dialog.set_values({ + user_image: this.user.image, + location: this.user.location, + interest: this.user.interest, + bio: this.user.bio + }); + edit_profile_dialog.show(); + } + + render_user_details() { + this.sidebar.empty().append(frappe.render_template('user_profile_sidebar', { + user_image: this.user.image, + user_abbr: this.user.abbr, + user_location: this.user.location, + user_interest: this.user.interest, + user_bio: this.user.bio, + })); + + this.setup_user_profile_links(); + } + + setup_user_profile_links() { + if (this.user_id !== frappe.session.user) { + this.wrapper.find('.profile-links').hide(); + } else { + this.wrapper.find('.edit-profile-link').on('click', () => { + this.edit_profile(); + }); + + this.wrapper.find('.user-settings-link').on('click', () => { + this.go_to_user_settings(); + }); + } + } + + get_user_rank() { + return frappe.xcall('frappe.desk.page.user_profile.user_profile.get_user_rank', { + user: this.user_id, + }).then(r => { + if (r.monthly_rank.length) this.month_rank = r.monthly_rank[0]; + if (r.all_time_rank.length) this.rank = r.all_time_rank[0]; + }); + } + + get_user_points() { + return frappe.xcall( + 'frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points', + { + user: this.user_id, + } + ).then(r => { + if (r[this.user_id]) { + this.energy_points = r[this.user_id].energy_points; + this.review_points = r[this.user_id].review_points; + } + }); + } + + render_points_and_rank() { + let $profile_details = this.wrapper.find('.user-stats'); + let $profile_details_wrapper = this.wrapper.find('.user-stats-detail'); + + const _get_stat_dom = (value, label, icon) => { + return `
        + ${frappe.utils.icon(icon, "lg", "no-stroke")} +
        +
        ${value}
        +
        ${label}
        +
        +
        `; + }; + + this.get_user_rank().then(() => { + this.get_user_points().then(() => { + let html = $(` + ${_get_stat_dom(this.energy_points, __('Energy Points'), "color-energy-points")} + ${_get_stat_dom(this.review_points, __('Review Points'), "color-review-points")} + ${_get_stat_dom(this.rank, __('Rank'), "color-rank")} + ${_get_stat_dom(this.month_rank, __('Monthly Rank'), "color-monthly-rank")} + `); + + $profile_details.append(html); + $profile_details_wrapper.removeClass("hide"); + }); + }); + } + + go_to_user_settings() { + frappe.set_route('Form', 'User', this.user_id); + } + + setup_user_activity_timeline() { + this.user_activity_timeline = new UserProfileTimeline({ + parent: this.wrapper.find('.recent-activity-list'), + footer: this.wrapper.find('.recent-activity-footer'), + user: this.user_id + }); + + this.user_activity_timeline.refresh(); + } +} + +class UserProfileTimeline extends BaseTimeline { + make() { + super.make(); + this.activity_start = 0; + this.activity_limit = 20; + this.setup_show_more_activity(); + } + prepare_timeline_contents() { + return this.get_user_activity_data().then((activities) => { + if (!activities.length) { + this.show_more_button.hide(); + this.timeline_wrapper.html(`
        ${__('No activities to show')}
        `); + return; + } + this.show_more_button.toggle(activities.length === this.activity_limit); + this.timeline_items = activities.map((activity) => this.get_activity_timeline_item(activity)); + }); + } + + get_user_activity_data() { + return frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_list', { + start: this.activity_start, + limit: this.activity_limit, + user: this.user + }); + } + + get_activity_timeline_item(data) { + let icon = data.type == 'Appreciation' ? 'clap': data.type == 'Criticism' ? 'criticize': null; + return { + icon: icon, + creation: data.creation, + is_card: true, + content: frappe.energy_points.format_history_log(data), + }; + } + + setup_show_more_activity() { + this.show_more_button = $(`${__('Show More Activity')}`); + this.show_more_button.hide(); + this.footer.append(this.show_more_button); + this.show_more_button.on('click', () => this.show_more_activity()); + } + + show_more_activity() { + this.activity_start += this.activity_limit; + this.get_user_activity_data().then(activities => { + if (!activities.length || activities.length < this.activity_limit) { + this.show_more_button.hide(); + } + let timeline_items = activities.map((activity) => this.get_activity_timeline_item(activity)); + timeline_items.map((item) => this.add_timeline_item(item, true)); + }); + } +} + +frappe.provide('frappe.ui'); +frappe.ui.UserProfile = UserProfile; diff --git a/frappe/desk/page/user_profile/user_profile_sidebar.html b/frappe/desk/page/user_profile/user_profile_sidebar.html index 77dae5edd0..4a35c6cf9c 100644 --- a/frappe/desk/page/user_profile/user_profile_sidebar.html +++ b/frappe/desk/page/user_profile/user_profile_sidebar.html @@ -1,23 +1,60 @@ \ No newline at end of file diff --git a/frappe/desk/report/todo/todo.py b/frappe/desk/report/todo/todo.py index a51d44fe08..f4fe2dc805 100644 --- a/frappe/desk/report/todo/todo.py +++ b/frappe/desk/report/todo/todo.py @@ -24,7 +24,7 @@ def execute(filters=None): for todo in todo_list: if todo.owner==frappe.session.user or todo.assigned_by==frappe.session.user: if todo.reference_type: - todo.reference = """%s: %s""" % (todo.reference_type, + todo.reference = """%s: %s""" % (todo.reference_type, todo.reference_name, todo.reference_type, todo.reference_name) else: todo.reference = None diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 9f5a5d84c8..3003385601 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -14,7 +14,7 @@ from frappe.core.doctype.access_log.access_log import make_access_log from frappe.utils import cstr, format_duration -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) @frappe.read_only() def get(): args = get_form_params() @@ -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 f4e6543844..6faa827dde 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -80,13 +80,15 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, is_whitelisted(frappe.get_attr(query)) frappe.response["values"] = frappe.call(query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict) - except Exception as e: + except frappe.exceptions.PermissionError as e: if frappe.local.conf.developer_mode: raise e else: frappe.respond_as_web_page(title='Invalid Method', html='Method not found', indicator_color='red', http_status_code=404) return + except Exception as e: + raise e elif not query and doctype in standard_queries: # from standard queries search_widget(doctype, txt, standard_queries[doctype][0], diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 811143be03..e0b6ca240a 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -1,5 +1,5 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt +# MIT License. See license.txt from __future__ import unicode_literals import frappe diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py new file mode 100644 index 0000000000..01b47ac106 --- /dev/null +++ b/frappe/desk/utils.py @@ -0,0 +1,23 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe + +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/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py index 1208a6c5c1..1ac2d19305 100644 --- a/frappe/email/doctype/document_follow/test_document_follow.py +++ b/frappe/email/doctype/document_follow/test_document_follow.py @@ -8,15 +8,15 @@ import unittest import frappe.desk.form.document_follow as document_follow class TestDocumentFollow(unittest.TestCase): - - def test_add_subscription_and_send_mail(self): + def test_document_follow(self): user = get_user() event_doc = get_event() event_doc.description = "This is a test description for sending mail" event_doc.save(ignore_version=False) - doc = document_follow.follow_document("Event", event_doc.name , user.name, force=True) + document_follow.unfollow_document("Event", event_doc.name, user.name) + doc = document_follow.follow_document("Event", event_doc.name, user.name) self.assertEquals(doc.user, user.name) document_follow.send_hourly_updates() @@ -45,12 +45,15 @@ def get_event(): return doc def get_user(): - doc = frappe.new_doc("User") - doc.email = "test@docsub.com" - doc.first_name = "Test" - doc.last_name = "User" - doc.send_welcome_email = 0 - doc.document_follow_notify = 1 - doc.document_follow_frequency = "Hourly" - doc.insert() + if frappe.db.exists('User', 'test@docsub.com'): + doc = frappe.get_doc('User', 'test@docsub.com') + else: + doc = frappe.new_doc("User") + doc.email = "test@docsub.com" + doc.first_name = "Test" + doc.last_name = "User" + doc.send_welcome_email = 0 + doc.document_follow_notify = 1 + doc.document_follow_frequency = "Hourly" + doc.insert() return doc \ 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_list.js b/frappe/email/doctype/email_account/email_account_list.js index f3bbd99e9b..5ec56fb3db 100644 --- a/frappe/email/doctype/email_account/email_account_list.js +++ b/frappe/email/doctype/email_account/email_account_list.js @@ -2,19 +2,19 @@ frappe.listview_settings["Email Account"] = { add_fields: ["default_incoming", "default_outgoing", "enable_incoming", "enable_outgoing"], get_indicator: function(doc) { if(doc.default_incoming && doc.default_outgoing) { - var color = (doc.enable_incoming && doc.enable_outgoing) ? "blue" : "darkgrey"; + var color = (doc.enable_incoming && doc.enable_outgoing) ? "blue" : "gray"; return [__("Default Sending and Inbox"), color, "default_incoming,=,Yes|default_outgoing,=,Yes"] } else if(doc.default_incoming) { - var color = doc.enable_incoming ? "blue" : "darkgrey"; + color = doc.enable_incoming ? "blue" : "gray"; return [__("Default Inbox"), color, "default_incoming,=,Yes"]; } else if(doc.default_outgoing) { - var color = doc.enable_outgoing ? "blue" : "darkgrey"; + color = doc.enable_outgoing ? "blue" : "gray"; return [__("Default Sending"), color, "default_outgoing,=,Yes"]; } else { - var color = doc.enable_incoming ? "blue" : "darkgrey"; + 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/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 27fcd0e453..c999f5f160 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -198,7 +198,7 @@ frappe.ui.form.on('Notification', { frappe.notification.setup_example_message(frm); if (frm.doc.channel === 'SMS' && frm.doc.__islocal) { frm.set_df_property('channel', - 'description', `To use SMS Channel, initialize SMS Settings.`); + 'description', `To use SMS Channel, initialize SMS Settings.`); } else { frm.set_df_property('channel', 'description', ` `); } 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 483d424464..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" @@ -129,7 +130,8 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= email_text_context += unsubscribe_link.text email_content = get_formatted_html(subject, message, - email_account=email_account, header=header) + email_account=email_account, header=header, + 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..3fcabb9495 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -17,7 +17,7 @@ class TestEmailBody(unittest.TestCase):

        Hey John Doe!

        This is embedded image you asked for

        - +
        ''' email_text = ''' @@ -25,7 +25,7 @@ Hey John Doe! This is the text version of this email ''' - img_path = os.path.abspath('assets/frappe/images/favicon.png') + img_path = os.path.abspath('assets/frappe/images/frappe-favicon.svg') with open(img_path, 'rb') as f: img_content = f.read() img_base64 = base64.b64encode(img_content).decode() @@ -77,12 +77,11 @@ This is the text version of this email def test_image(self): img_signature = ''' -Content-Type: image/png +Content-Type: image/svg+xml MIME-Version: 1.0 Content-Transfer-Encoding: base64 -Content-Disposition: inline; filename="favicon.png" +Content-Disposition: inline; filename="frappe-favicon.svg" ''' - self.assertTrue(img_signature in self.email_string) self.assertTrue(self.img_base64 in self.email_string) @@ -117,7 +116,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> def test_replace_filename_with_cid(self): original_message = '''
        - test + test
        ''' @@ -138,7 +137,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 +153,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/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/hooks.py b/frappe/hooks.py index ea0a91a639..3e206f0ad3 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -10,7 +10,7 @@ app_icon = "octicon octicon-circuit-board" app_color = "orange" source_link = "https://github.com/frappe/frappe" app_license = "MIT" -app_logo_url = '/assets/frappe/images/frappe-framework-logo.png' +app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg' develop_version = '13.x.x-develop' @@ -29,18 +29,17 @@ page_js = { # website app_include_js = [ - "assets/js/libs.min.js", - "assets/js/desk.min.js", - "assets/js/list.min.js", - "assets/js/form.min.js", - "assets/js/control.min.js", - "assets/js/report.min.js", + "/assets/js/libs.min.js", + "/assets/js/desk.min.js", + "/assets/js/list.min.js", + "/assets/js/form.min.js", + "/assets/js/control.min.js", + "/assets/js/report.min.js", ] app_include_css = [ - "assets/css/desk.min.css", - "assets/css/list.min.css", - "assets/css/form.min.css", - "assets/css/report.min.css", + "/assets/css/desk.min.css", + "/assets/css/list.min.css", + "/assets/css/report.min.css", ] doctype_js = { @@ -59,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/installer.py b/frappe/installer.py index a11c8dfbfa..0cd5b136ae 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -249,7 +249,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) linked_doctypes = frappe.get_all( "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"] ) - ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"] + ordered_doctypes = ["Workspace", "Report", "Page", "Web Form"] all_doctypes_with_linked_modules = ordered_doctypes + [ doctype.parent for doctype in linked_doctypes diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json deleted file mode 100644 index 97e2b29d1a..0000000000 --- a/frappe/integrations/desk_page/integrations/integrations.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "cards": [ - { - "hidden": 0, - "label": "Backup", - "links": "[\n {\n \"description\": \"Dropbox backup settings\",\n \"label\": \"Dropbox Settings\",\n \"name\": \"Dropbox Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"S3 Backup Settings\",\n \"label\": \"S3 Backup Settings\",\n \"name\": \"S3 Backup Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Backup.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Google Services", - "links": "[\n {\n \"description\": \"Google API Settings.\",\n \"label\": \"Google Settings\",\n \"name\": \"Google Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Contacts Integration.\",\n \"label\": \"Google Contacts\",\n \"name\": \"Google Contacts\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Calendar Integration.\",\n \"label\": \"Google Calendar\",\n \"name\": \"Google Calendar\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Integration.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Authentication", - "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n ,\n {\n \"description\": \"Connect to any OAuth Provider\",\n \"label\": \"Connected App\",\n \"name\": \"Connected App\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Payments", - "links": "[\n {\n \"description\": \"Braintree payment gateway settings\",\n \"label\": \"Braintree Settings\",\n \"name\": \"Braintree Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"PayPal payment gateway settings\",\n \"label\": \"PayPal Settings\",\n \"name\": \"PayPal Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Razorpay Payment gateway settings\",\n \"label\": \"Razorpay Settings\",\n \"name\": \"Razorpay Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stripe payment gateway settings\",\n \"label\": \"Stripe Settings\",\n \"name\": \"Stripe Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Paytm payment gateway settings\",\n \"label\": \"Paytm Settings\",\n \"name\": \"Paytm Settings\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Settings", - "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"SMS Settings for sending sms\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n }\n]" - } - ], - "category": "Administration", - "charts": [], - "creation": "2020-03-02 15:16:18.714190", - "developer_mode_only": 0, - "disable_user_customization": 1, - "docstatus": 0, - "doctype": "Desk Page", - "extends_another_page": 0, - "hide_custom": 0, - "idx": 0, - "is_standard": 1, - "label": "Integrations", - "modified": "2020-10-28 10:25:54.792363", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Integrations", - "owner": "Administrator", - "pin_to_bottom": 0, - "pin_to_top": 0, - "shortcuts": [] -} \ No newline at end of file diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 4d8acb9b59..6faa542a60 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -10,7 +10,47 @@ 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 -test_dependencies = ['Connected App', 'OAuth Client', 'User'] + +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): @@ -26,37 +66,56 @@ class TestConnectedApp(unittest.TestCase): just endpoints) are stored in "Social Login Key" so we get them from there. """ - self.user_name = 'test@example.com' + self.user_name = 'test-connected-app@example.com' self.user_password = 'Eastern_43A1W' - connected_app = frappe.get_last_doc('Connected App') - redirect_uri = connected_app.get('redirect_uri') - - web_application_client = frappe.get_last_doc('OAuth Client') - web_application_client.update({ - 'redirect_uris': redirect_uri, - 'default_redirect_uri': redirect_uri - }) - web_application_client.save() - + 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') - connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) - connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) - connected_app.client_id = web_application_client.get('client_id') - connected_app.client_secret = web_application_client.get('client_secret') - self.connected_app = connected_app.save() + 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() - session.post(urljoin(self.base_url, '/api/method/login'), data={ - 'usr': self.user_name, - 'pwd': self.user_password - }) + + # 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) @@ -65,10 +124,39 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.connected_app.get_token_cache(self.user_name) - token = token_cache.get_password('access_token') + 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/google_calendar/google_calendar.js b/frappe/integrations/doctype/google_calendar/google_calendar.js index f941cf0575..f30c52b2f2 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.js +++ b/frappe/integrations/doctype/google_calendar/google_calendar.js @@ -4,7 +4,7 @@ frappe.ui.form.on("Google Calendar", { refresh: function(frm) { if (frm.is_new()) { - frm.dashboard.set_headline(__("To use Google Calendar, enable {0}.", [`${__('Google Settings')}`])); + frm.dashboard.set_headline(__("To use Google Calendar, enable {0}.", [`${__('Google Settings')}`])); } frappe.realtime.on("import_google_calendar", (data) => { diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 4a9acd9e84..fbedd75029 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -120,7 +120,7 @@ def authorize_access(g_calendar, reauthorize=None): frappe.db.commit() frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/desk#Form/{0}/{1}".format(quote("Google Calendar"), quote(google_calendar.name)) + frappe.local.response["location"] = "/app/Form/{0}/{1}".format(quote("Google Calendar"), quote(google_calendar.name)) frappe.msgprint(_("Google Calendar has been configured.")) except Exception as e: diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.js b/frappe/integrations/doctype/google_contacts/google_contacts.js index af194d4978..7cbef46699 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.js +++ b/frappe/integrations/doctype/google_contacts/google_contacts.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Google Contacts', { refresh: function(frm) { if (!frm.doc.enable) { - frm.dashboard.set_headline(__("To use Google Contacts, enable {0}.", [`${__('Google Settings')}`])); + frm.dashboard.set_headline(__("To use Google Contacts, enable {0}.", [`${__('Google Settings')}`])); } frappe.realtime.on('import_google_contacts', (data) => { diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 6455623281..4c8c3b67f6 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -79,7 +79,7 @@ def authorize_access(g_contact, reauthorize=None): frappe.db.commit() frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/desk#Form/Google%20Contacts/{}".format(google_contact.name) + frappe.local.response["location"] = "/app/Form/Google%20Contacts/{}".format(google_contact.name) frappe.msgprint(_("Google Contacts has been configured.")) except Exception as e: diff --git a/frappe/integrations/doctype/google_drive/google_drive.js b/frappe/integrations/doctype/google_drive/google_drive.js index f184c6d75c..c314d02e7e 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.js +++ b/frappe/integrations/doctype/google_drive/google_drive.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Google Drive', { refresh: function(frm) { if (!frm.doc.enable) { - frm.dashboard.set_headline(__("To use Google Drive, enable {0}.", [`${__('Google Settings')}`])); + frm.dashboard.set_headline(__("To use Google Drive, enable {0}.", [`${__('Google Settings')}`])); } frappe.realtime.on("upload_to_google_drive", (data) => { diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index c1c73d7726..859c769018 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -88,7 +88,7 @@ def authorize_access(reauthorize=None): frappe.db.commit() frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/desk#Form/{0}".format(quote("Google Drive")) + frappe.local.response["location"] = "/app/Form/{0}".format(quote("Google Drive")) frappe.msgprint(_("Google Drive has been configured.")) except Exception as e: diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.json b/frappe/integrations/doctype/social_login_key/social_login_key.json index 6c0fbdb26c..bb97d8f625 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.json +++ b/frappe/integrations/doctype/social_login_key/social_login_key.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2017-11-18 15:36:09.676722", "doctype": "DocType", @@ -82,6 +83,7 @@ "label": "Identity Details" }, { + "depends_on": "eval:doc.social_login_provider==\"Custom\"", "fieldname": "icon", "fieldtype": "Data", "label": "Icon" @@ -157,7 +159,9 @@ "label": "User ID Property" } ], - "modified": "2019-12-03 13:13:46.989099", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-30 14:37:13.616002", "modified_by": "Administrator", "module": "Integrations", "name": "Social Login Key", diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py index 81df3cca97..d84e6ef11d 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/social_login_key.py @@ -20,6 +20,7 @@ class SocialLoginKey(Document): self.name = frappe.scrub(self.provider_name) def validate(self): + self.set_icon() if self.custom_base_url and not self.base_url: frappe.throw(_("Please enter Base URL"), exc=BaseUrlNotSetError) if not self.authorize_url: @@ -33,6 +34,21 @@ class SocialLoginKey(Document): if self.enable_social_login and not self.client_secret: frappe.throw(_("Please enter Client Secret before social login is enabled"), exc=ClientSecretNotSetError) + def set_icon(self): + icon_map = { + "Google": "google.svg", + "Frappe": "frappe.svg", + "Facebook": "facebook.svg", + "Office 365": "office_365.svg", + "GitHub": "github.svg", + "Salesforce": "salesforce.svg", + "fairlogin": "fair.svg" + } + + if self.provider_name in icon_map: + icon_file = icon_map[self.provider_name] + self.icon = '/assets/frappe/icons/social/{0}'.format(icon_file) + def get_social_login_provider(self, provider, initialize=False): providers = {} @@ -108,7 +124,7 @@ class SocialLoginKey(Document): "provider_name": "Frappe", "enable_social_login": 1, "custom_base_url": 1, - "icon":"/assets/frappe/images/favicon.png", + "icon":"/assets/frappe/images/frappe-favicon.svg", "redirect_url": "/api/method/frappe.www.login.login_via_frappe", "api_endpoint": "/api/method/frappe.integrations.oauth2.openid_profile", "api_endpoint_args":None, diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json new file mode 100644 index 0000000000..db96304207 --- /dev/null +++ b/frappe/integrations/workspace/integrations/integrations.json @@ -0,0 +1,260 @@ +{ + "category": "Administration", + "charts": [], + "creation": "2020-03-02 15:16:18.714190", + "developer_mode_only": 0, + "disable_user_customization": 1, + "docstatus": 0, + "doctype": "Workspace", + "extends_another_page": 0, + "hide_custom": 0, + "icon": "integration", + "idx": 0, + "is_standard": 1, + "label": "Integrations", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Backup", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dropbox Settings", + "link_to": "Dropbox Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "S3 Backup Settings", + "link_to": "S3 Backup Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Drive", + "link_to": "Google Drive", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Google Services", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Settings", + "link_to": "Google Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Contacts", + "link_to": "Google Contacts", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Calendar", + "link_to": "Google Calendar", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Google Drive", + "link_to": "Google Drive", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Authentication", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Social Login Key", + "link_to": "Social Login Key", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "LDAP Settings", + "link_to": "LDAP Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "OAuth Client", + "link_to": "OAuth Client", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "OAuth Provider Settings", + "link_to": "OAuth Provider Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payments", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Braintree Settings", + "link_to": "Braintree Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "PayPal Settings", + "link_to": "PayPal Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Razorpay Settings", + "link_to": "Razorpay Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Stripe Settings", + "link_to": "Stripe Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Paytm Settings", + "link_to": "Paytm Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Webhook", + "link_to": "Webhook", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Slack Webhook URL", + "link_to": "Slack Webhook URL", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Twilio Settings", + "link_to": "Twilio Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Settings", + "link_to": "SMS Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2020-12-01 13:38:39.706680", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Integrations", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index c39a73ccd7..af06696621 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -118,7 +118,7 @@ core_doctypes_list = ( 'Customize Form Field', 'Property Setter', 'Custom Field', - 'Custom Script' + 'Client Script' ) log_types = ( diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 7a90ecaca5..295585665f 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -69,13 +69,13 @@ def get_controller(doctype): if frappe.local.dev_server: return _get_controller() - + 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") @@ -94,6 +94,14 @@ class BaseDocument(object): return self._meta def update(self, d): + """ Update multiple fields of a doctype using a dictionary of key-value pairs. + + Example: + doc.update({ + "user": "admin", + "balance": 42000 + }) + """ if "doctype" in d: self.set("doctype", d.get("doctype")) @@ -159,6 +167,15 @@ class BaseDocument(object): del self.__dict__[key] def append(self, key, value=None): + """ Append an item to a child table. + + Example: + doc.append("childtable", { + "child_table_field": "value", + "child_table_int_field": 0, + ... + }) + """ if value==None: value={} if isinstance(value, (dict, BaseDocument)): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index c799586d61..8eac75eb65 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -597,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) @@ -698,7 +698,7 @@ class DatabaseQuery(object): if c: conditions.append(c) - permision_script_name = get_server_script_map().get("permission_query").get(self.doctype) + permision_script_name = get_server_script_map().get("permission_query", {}).get(self.doctype) if permision_script_name: script = frappe.get_doc("Server Script", permision_script_name) condition = script.get_permission_query_conditions(self.user) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 15de673e4b..d0e0a6fb1a 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -68,7 +68,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa check_permission_and_not_submitted(doc) frappe.db.sql("delete from `tabCustom Field` where dt = %s", name) - frappe.db.sql("delete from `tabCustom Script` where dt = %s", name) + frappe.db.sql("delete from `tabClient Script` where dt = %s", name) frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name) frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name) frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name) @@ -292,8 +292,8 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): raise_link_exists_exception(doc, reference_doctype, reference_docname, at_position) def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=''): - doc_link = '{1}'.format(doc.doctype, doc.name) - reference_link = '{1}'.format(reference_doctype, reference_docname) + doc_link = '{1}'.format(doc.doctype, doc.name) + reference_link = '{1}'.format(reference_doctype, reference_docname) #hack to display Single doctype only once in message if reference_doctype == reference_docname: diff --git a/frappe/model/document.py b/frappe/model/document.py index 9efd8b6c94..3ecc335cdd 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1015,6 +1015,8 @@ class Document(BaseDocument): def notify_update(self): """Publish realtime that the current document is modified""" + if frappe.flags.in_patch: return + frappe.publish_realtime("doc_update", {"modified": self.modified, "doctype": self.doctype, "name": self.name}, doctype=self.doctype, docname=self.name, after_commit=True) @@ -1190,8 +1192,8 @@ class Document(BaseDocument): doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.parentfield))) def get_url(self): - """Returns Desk URL for this document. `/desk#Form/{doctype}/{name}`""" - return "/desk#Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name) + """Returns Desk URL for this document. `/app/Form/{doctype}/{name}`""" + return "/app/Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name) def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None): """Add a comment to this document. diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 88ed1a7e78..5dc7ca2d4d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -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. diff --git a/frappe/model/naming.py b/frappe/model/naming.py index c2e074990e..e954debe6f 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -67,7 +67,6 @@ def set_new_name(doc): frappe.get_meta(doc.doctype).get_field("name_case") ) - def set_name_from_naming_options(autoname, doc): """ Get a name based on the autoname field option diff --git a/frappe/model/sync.py b/frappe/model/sync.py index b7d9d4d548..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_card"), - ("desk", "desk_chart"), - ("desk", "desk_shortcut"), - ("desk", "desk_page")): + ("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,13 +77,13 @@ 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 document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'web_template', 'notification', 'print_style', - 'data_migration_mapping', 'data_migration_plan', 'desk_page', + 'data_migration_mapping', 'data_migration_plan', 'workspace', 'onboarding_step', 'module_onboarding'] for doctype in document_types: diff --git a/frappe/patches.txt b/frappe/patches.txt index 1a086303ba..5400c96354 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -15,6 +15,7 @@ execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 execute:frappe.reload_doc('core', 'doctype', 'comment') frappe.patches.v8_0.drop_is_custom_from_docperm +execute:frappe.reload_doc('core', 'doctype', 'document_naming_rule', force=True) execute:frappe.reload_doc('core', 'doctype', 'module_def') #2020-08-28 execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 execute:frappe.reload_doc('email', 'doctype', 'document_follow') @@ -208,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 @@ -243,7 +244,6 @@ frappe.patches.v12_0.set_primary_key_in_series execute:frappe.delete_doc("Page", "modules", ignore_missing=True) frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.setup_comments_from_communications -frappe.patches.v12_0.init_desk_settings #16-05-2019 frappe.patches.v12_0.replace_null_values_in_tables frappe.patches.v12_0.reset_home_settings frappe.patches.v12_0.update_print_format_type @@ -265,7 +265,6 @@ frappe.patches.v12_0.copy_to_parent_for_tags frappe.patches.v12_0.create_notification_settings_for_user frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 frappe.patches.v12_0.setup_email_linking -frappe.patches.v12_0.fix_home_settings_for_all_users frappe.patches.v12_0.change_existing_dashboard_chart_filters frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13 execute:frappe.delete_doc("Test Runner") @@ -299,13 +298,14 @@ frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart -frappe.patches.v13_0.add_standard_navbar_items +frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15 frappe.patches.v13_0.generate_theme_files_in_public_folder frappe.patches.v13_0.increase_password_length frappe.patches.v12_0.fix_email_id_formatting frappe.patches.v13_0.add_toggle_width_in_navbar_settings frappe.patches.v13_0.rename_notification_fields frappe.patches.v13_0.remove_duplicate_navbar_items +frappe.patches.v13_0.set_social_icons frappe.patches.v12_0.set_default_password_reset_limit execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) frappe.patches.v13_0.set_route_for_blog_category @@ -316,5 +316,18 @@ 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 +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 +frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings +frappe.patches.v13_0.rename_custom_client_script diff --git a/frappe/patches/v12_0/fix_home_settings_for_all_users.py b/frappe/patches/v12_0/fix_home_settings_for_all_users.py deleted file mode 100644 index e26cbd9eef..0000000000 --- a/frappe/patches/v12_0/fix_home_settings_for_all_users.py +++ /dev/null @@ -1,41 +0,0 @@ -import frappe -from frappe.config import get_modules_from_all_apps_for_user -import json -def execute(): - users = frappe.get_all('User', fields=['name', 'home_settings']) - - for user in users: - - if not user.home_settings: - continue - - home_settings = json.loads(user.home_settings) - - modules_by_category = home_settings.get('modules_by_category') - if not modules_by_category: - continue - visible_modules = [] - category_to_check = [] - - for category, modules in modules_by_category.items(): - visible_modules += modules - category_to_check.append(category) - - all_modules = get_modules_from_all_apps_for_user(user.name) - all_modules = set([m.get('name') or m.get('module_name') or m.get('label') \ - for m in all_modules if m.get('category') in category_to_check]) - - hidden_modules = home_settings.get("hidden_modules", []) - - modules_in_home_settings = set(visible_modules + hidden_modules) - - all_modules = all_modules.union(modules_in_home_settings) - - missing_modules = all_modules - modules_in_home_settings - - if missing_modules: - home_settings['hidden_modules'] = hidden_modules + list(missing_modules) - home_settings = json.dumps(home_settings) - frappe.set_value('User', user.name, 'home_settings', home_settings) - - frappe.cache().delete_key('home_settings') diff --git a/frappe/patches/v12_0/reset_home_settings.py b/frappe/patches/v12_0/reset_home_settings.py index db16c31f15..e4b9de6cb2 100644 --- a/frappe/patches/v12_0/reset_home_settings.py +++ b/frappe/patches/v12_0/reset_home_settings.py @@ -1,6 +1,7 @@ import frappe def execute(): + frappe.reload_doc('core', 'doctype', 'user') frappe.db.sql(''' UPDATE `tabUser` SET `home_settings` = '' diff --git a/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py b/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py new file mode 100644 index 0000000000..29b99464b5 --- /dev/null +++ b/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + navbar_settings = frappe.get_single("Navbar Settings") + + if frappe.db.exists('Navbar Item', {'item_label': 'Toggle Theme'}): + return + + for navbar_item in navbar_settings.settings_dropdown[6:]: + navbar_item.idx = navbar_item.idx + 1 + + navbar_settings.append('settings_dropdown', { + 'item_label': 'Toggle Theme', + 'item_type': 'Action', + 'action': 'new frappe.ui.ThemeSwitcher().show()', + 'is_standard': 1, + 'idx': 7 + }) + + navbar_settings.save() \ No newline at end of file diff --git a/frappe/patches/v13_0/cleanup_desk_cards.py b/frappe/patches/v13_0/cleanup_desk_cards.py new file mode 100644 index 0000000000..6ac8604041 --- /dev/null +++ b/frappe/patches/v13_0/cleanup_desk_cards.py @@ -0,0 +1,68 @@ +import frappe +from six import string_types +from json import loads +from frappe.desk.doctype.workspace.workspace import get_link_type, get_report_type + +def execute(): + frappe.reload_doc('desk', 'doctype', 'workspace') + + pages = frappe.db.sql("Select `name` from `tabDesk Page`") + # pages = frappe.get_all("Workspace", filters={"is_standard": 0}, pluck="name") + + for page in pages: + rebuild_links(page[0]) + + frappe.delete_doc("DocType", "Desk Card") + +def rebuild_links(page): + # Empty links table + + try: + doc = frappe.get_doc("Workspace", page) + except frappe.DoesNotExistError: + db_doc = get_doc_from_db(page) + + doc = frappe.get_doc(db_doc) + doc.insert(ignore_permissions=True) + + doc.links = [] + + for card in get_all_cards(page): + if isinstance(card.links, string_types): + links = loads(card.links) + else: + links = card.links + + doc.append('links', { + "label": card.label, + "type": "Card Break", + "icon": card.icon, + "hidden": card.hidden or False + }) + + for link in links: + if not frappe.db.exists(get_link_type(link.get('type')), link.get('name')): + continue + + doc.append('links', { + "label": link.get('label') or link.get('name'), + "type": "Link", + "link_type": get_link_type(link.get('type')), + "link_to": link.get('name'), + "onboard": link.get('onboard'), + "dependencies": ', '.join(link.get('dependencies', [])), + "is_query_report": get_report_type(link.get('name')) if link.get('type').lower() == "report" else 0 + }) + + try: + doc.save(ignore_permissions=True) + except frappe.LinkValidationError: + print(doc.as_dict()) + +def get_doc_from_db(page): + result = frappe.db.sql("SELECT * FROM `tabDesk Page` WHERE name=%s", [page], as_dict=True) + if result: + return result[0].update({"doctype": "Workspace"}) + +def get_all_cards(page): + return frappe.db.get_all("Desk Card", filters={"parent": page}, fields=['*'], order_by="idx") \ No newline at end of file diff --git a/frappe/patches/v13_0/rename_custom_client_script.py b/frappe/patches/v13_0/rename_custom_client_script.py new file mode 100644 index 0000000000..718f1f6a46 --- /dev/null +++ b/frappe/patches/v13_0/rename_custom_client_script.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + if frappe.db.exists("DocType", "Client Script"): + return + + frappe.rename_doc("DocType", "Custom Script", "Client Script") + frappe.reload_doctype("Client Script", force=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 new file mode 100644 index 0000000000..6483fc380c --- /dev/null +++ b/frappe/patches/v13_0/rename_desk_page_to_workspace.py @@ -0,0 +1,21 @@ +import frappe +from frappe.model.rename_doc import rename_doc + +def execute(): + 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/rename_list_view_setting_to_list_view_settings.py b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py new file mode 100644 index 0000000000..fcf8afc826 --- /dev/null +++ b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + if frappe.db.table_exists('List View Setting'): + existing_list_view_settings = frappe.get_all('List View Settings', as_list=True) + for list_view_setting in frappe.get_all('List View Setting', fields = ['disable_count', 'disable_sidebar_stats', 'disable_auto_refresh', 'name']): + name = list_view_setting.pop('name') + if name not in [x[0] for x in existing_list_view_settings]: + list_view_setting['doctype'] = 'List View Settings' + list_view_settings = frappe.get_doc(list_view_setting) + # setting name here is necessary because autoname is set as prompt + list_view_settings.name = name + list_view_settings.insert() + frappe.delete_doc("DocType", "List View Setting", force=True) + frappe.db.commit() diff --git a/frappe/patches/v13_0/set_social_icons.py b/frappe/patches/v13_0/set_social_icons.py new file mode 100644 index 0000000000..b1c63b48e0 --- /dev/null +++ b/frappe/patches/v13_0/set_social_icons.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + providers = frappe.get_all("Social Login Key") + + for provider in providers: + doc = frappe.get_doc("Social Login Key", provider) + doc.set_icon() + doc.save() \ No newline at end of file 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 new file mode 100644 index 0000000000..93bf5c766e --- /dev/null +++ b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py @@ -0,0 +1,15 @@ +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: + if page.extends and page.for_user: + if not default_icon.get(page.extends): + default_icon[page.extends] = frappe.db.get_value("Desk Page", page.extends, "icon") + + icon = default_icon.get(page.extends) + frappe.db.set_value("Desk Page", page.name, "icon", icon) \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index a45fbdcd06..abb1f6653a 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -398,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 @@ -413,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/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 491d959755..3a3b14faad 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import is_image from frappe.model.document import Document +from frappe import _ class LetterHead(Document): def before_insert(self): @@ -13,14 +14,20 @@ class LetterHead(Document): def validate(self): self.set_image() - if not self.is_default: - if not frappe.db.sql("""select count(*) from `tabLetter Head` where ifnull(is_default,0)=1"""): + self.validate_disabled_and_default() + + def validate_disabled_and_default(self): + if self.disabled and self.is_default: + frappe.throw(_("Letter Head cannot be both disabled and default")) + + if not self.is_default and not self.disabled: + if not frappe.db.exists('Letter Head', dict(is_default=1)): self.is_default = 1 def set_image(self): if self.source=='Image': if self.image and is_image(self.image): - self.content = ''.format(self.image) + self.content = ''.format(self.image) frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True) else: frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange') diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 3a47fb554f..92d4a67d14 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -154,7 +154,7 @@ "fieldname": "font", "fieldtype": "Select", "label": "Font", - "options": "Default\nArial\nHelvetica\nVerdana\nMonospace" + "options": "Default\nHelvetica Neue\nArial\nHelvetica\nVerdana\nMonospace" }, { "depends_on": "eval:!doc.raw_printing", diff --git a/frappe/printing/doctype/print_format/test_print_format.py b/frappe/printing/doctype/print_format/test_print_format.py index e8375ae5e7..7e30bda23e 100644 --- a/frappe/printing/doctype/print_format/test_print_format.py +++ b/frappe/printing/doctype/print_format/test_print_format.py @@ -11,8 +11,8 @@ test_records = frappe.get_test_records('Print Format') class TestPrintFormat(unittest.TestCase): def test_print_user(self, style=None): print_html = frappe.get_print("User", "Administrator", style=style) - self.assertTrue("" in print_html) - self.assertTrue(re.findall('
        [\s]*administrator[\s]*
        ', print_html)) + self.assertTrue("" in print_html) + self.assertTrue(re.findall('
        [\s]*administrator[\s]*
        ', print_html)) return print_html def test_print_user_standard(self): diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index f93ad0ee5a..d64cb4c6d3 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -170,7 +170,7 @@ "fieldname": "font", "fieldtype": "Select", "label": "Font", - "options": "Default\nArial\nHelvetica\nVerdana\nMonospace" + "options": "Default\nHelvetica Neue\nArial\nHelvetica\nInter\nVerdana\nMonospace" }, { "description": "In points. Default is 9.", @@ -180,9 +180,10 @@ } ], "icon": "fa fa-cog", + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-02 16:14:47.470668", + "modified": "2020-10-22 23:42:09.471022", "modified_by": "Administrator", "module": "Printing", "name": "Print Settings", diff --git a/frappe/printing/page/print/__init__.py b/frappe/printing/page/print/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js new file mode 100644 index 0000000000..0ae8786e95 --- /dev/null +++ b/frappe/printing/page/print/print.js @@ -0,0 +1,756 @@ +frappe.pages['print'].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + }); + + let print_view = new frappe.ui.form.PrintView(wrapper); + + $(wrapper).bind('show', () => { + const route = frappe.get_route(); + const doctype = route[1]; + const docname = route[2]; + if (!frappe.route_options || !frappe.route_options.frm) { + frappe.model.with_doc(doctype, docname, () => { + let frm = { doctype: doctype, docname: docname }; + frm.doc = frappe.get_doc(doctype, docname); + frappe.model.with_doctype(doctype, () => { + frm.meta = frappe.get_meta(route[1]); + print_view.show(frm); + }); + }); + } else { + print_view.frm = frappe.route_options.frm; + frappe.route_options.frm = null; + print_view.show(print_view.frm); + } + }); +}; + +frappe.ui.form.PrintView = class { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + this.make(); + } + + make() { + this.print_wrapper = this.page.main.empty().html( + `` + ); + + this.print_settings = frappe.model.get_doc( + ':Print Settings', + 'Print Settings' + ); + this.setup_toolbar(); + this.setup_menu(); + this.setup_sidebar(); + this.setup_keyboard_shortcuts(); + } + + set_title() { + this.page.set_title(this.frm.docname); + } + + setup_toolbar() { + this.page.set_primary_action( + __('Print'), + () => this.printit(), 'printer' + ); + + this.page.add_button( + __('Full Page'), + () => this.render_page('/printview?'), + { icon: 'full-page' } + ); + + this.page.add_button( + __('PDF'), + () => this.render_page('/api/method/frappe.utils.print_format.download_pdf?'), + { icon: 'small-file' } + ); + + this.page.add_button( + frappe.utils.icon('refresh'), + () => this.refresh_print_format() + ); + } + + setup_sidebar() { + this.sidebar = this.page.sidebar.addClass('print-preview-sidebar'); + + this.print_sel = this.add_sidebar_item( + { + fieldtype: 'Select', + fieldname: 'print_format', + label: 'Print Format', + options: [this.get_default_option_for_select(__('Select Print Format'))], + change: () => this.refresh_print_format(), + default: __('Select Print Format') + }, + ).$input; + + this.language_sel = this.add_sidebar_item( + { + fieldtype: 'Select', + fieldname: 'language', + placeholder: 'Language', + options: [ + this.get_default_option_for_select(__('Select Language')), + ...this.get_language_options() + ], + default: __('Select Language'), + change: () => { + this.set_user_lang(); + this.preview(); + }, + }, + ).$input; + + this.letterhead_selector = this.add_sidebar_item( + { + fieldtype: 'Select', + fieldname: 'letterhead', + label: __('Select Letterhead'), + options: [ + this.get_default_option_for_select(__('Select Letterhead')), + __('No Letterhead') + ], + change: () => this.preview(), + default: this.print_settings.with_letterhead + ? __('No Letterhead') + : __('Select Letterhead') + }, + ).$input; + + this.sidebar_dynamic_section = $( + `
        ` + ).appendTo(this.sidebar); + } + + add_sidebar_item(df, is_dynamic) { + if (df.fieldtype == 'Select') { + df.input_class = 'btn btn-default btn-sm'; + } + + let field = frappe.ui.form.make_control({ + df: df, + parent: is_dynamic ? this.sidebar_dynamic_section : this.sidebar, + render_input: 1, + }); + + if (df.default != null) { + field.set_input(df.default); + } + + return field; + } + + get_default_option_for_select(value) { + return { + label: value, + value: value, + disabled: true + }; + } + + setup_menu() { + this.page.clear_menu(); + + this.page.add_menu_item(__('Print Settings'), () => { + frappe.set_route('Form', 'Print Settings'); + }); + + if ( + frappe.model.get_doc(':Print Settings', 'Print Settings') + .enable_raw_printing == '1' + ) { + this.page.add_menu_item(__('Raw Printing Setting'), () => { + this.printer_setting_dialog(); + }); + } + + if (frappe.user.has_role('System Manager')) { + this.page.add_menu_item(__('Customize'), () => + this.edit_print_format() + ); + } + } + + show(frm) { + this.frm = frm; + this.set_title(); + this.set_breadcrumbs(); + this.setup_customize_dialog(); + + let tasks = [ + this.refresh_print_options, + this.set_default_print_language, + this.set_letterhead_options, + this.preview, + ].map((fn) => fn.bind(this)); + + this.setup_additional_settings(); + return frappe.run_serially(tasks); + } + + set_breadcrumbs() { + frappe.breadcrumbs.add(this.frm.meta.module, this.frm.doctype); + } + + setup_additional_settings() { + this.additional_settings = {}; + this.sidebar_dynamic_section.empty(); + frappe + .xcall('frappe.printing.page.print.print.get_print_settings_to_show', { + doctype: this.frm.doc.doctype, + docname: this.frm.doc.name + }) + .then((settings) => this.add_settings_to_sidebar(settings)); + } + + add_settings_to_sidebar(settings) { + for (let df of settings) { + let field = this.add_sidebar_item({ + ...df, + change: () => { + const val = field.get_value(); + this.additional_settings[field.df.fieldname] = val; + this.preview(); + }, + }, true); + } + } + + edit_print_format() { + let print_format = this.get_print_format(); + let is_custom_format = + print_format.name && + print_format.print_format_builder && + print_format.standard === 'No'; + let is_standard_but_editable = + print_format.name && print_format.custom_format; + + if (is_standard_but_editable) { + frappe.set_route('Form', 'Print Format', print_format.name); + return; + } + if (is_custom_format) { + frappe.set_route('print-format-builder', print_format.name); + return; + } + // start a new print format + frappe.prompt( + [ + { + label: __('New Print Format Name'), + fieldname: 'print_format_name', + fieldtype: 'Data', + reqd: 1, + }, + { + label: __('Based On'), + fieldname: 'based_on', + fieldtype: 'Read Only', + default: print_format.name || 'Standard', + }, + ], + (data) => { + frappe.route_options = { + make_new: true, + doctype: this.frm.doctype, + name: data.print_format_name, + based_on: data.based_on, + }; + frappe.set_route('print-format-builder'); + }, + __('New Custom Print Format'), + __('Start') + ); + } + + refresh_print_format() { + this.set_default_print_language(); + this.toggle_raw_printing(); + this.preview(); + } + + // bind_events () { + // // // hide print view on pressing escape, only if there is no focus on any input + // // $(document).on("keydown", function (e) { + // // if (e.which === 27 && me.frm && e.target === document.body) { + // // me.hide(); + // // } + // // }); + // } + + setup_customize_dialog() { + let print_format = this.get_print_format(); + $(document).on('new-print-format', (e) => { + this.refresh_print_options(); + if (e.print_format) { + this.print_sel.val(e.print_format); + } + // start a new print format + frappe.prompt( + [ + { + label: __('New Print Format Name'), + fieldname: 'print_format_name', + fieldtype: 'Data', + reqd: 1, + }, + { + label: __('Based On'), + fieldname: 'based_on', + fieldtype: 'Read Only', + default: print_format.name || 'Standard', + }, + ], + (data) => { + frappe.route_options = { + make_new: true, + doctype: this.frm.doctype, + name: data.print_format_name, + based_on: data.based_on, + }; + frappe.set_route('print-format-builder'); + }, + __('New Custom Print Format'), + __('Start') + ); + }); + } + + setup_keyboard_shortcuts() { + this.wrapper.find('.print-toolbar a.btn-default').each((i, el) => { + frappe.ui.keys.get_shortcut_group(this.frm.page).add($(el)); + }); + } + + set_letterhead_options() { + let letterhead_options = [ + this.get_default_option_for_select(__('Select Letterhead')), + __('No Letterhead') + ]; + let default_letterhead; + let doc_letterhead = this.frm.doc.letter_head; + + return frappe.db + .get_list('Letter Head', { fields: ['name', 'is_default'] }) + .then((letterheads) => { + this.letterhead_selector.empty(); + letterheads.map((letterhead) => { + if (letterhead.is_default) default_letterhead = letterhead.name; + return letterhead_options.push(letterhead.name); + }); + + this.letterhead_selector.add_options(letterhead_options); + let selected_letterhead = doc_letterhead || default_letterhead; + if (selected_letterhead) + this.letterhead_selector.val(selected_letterhead); + }); + } + + set_user_lang() { + this.lang_code = this.language_sel.val(); + } + + get_language_options() { + return frappe.get_languages(); + } + + set_default_print_language() { + let print_format = this.get_print_format(); + this.lang_code = + print_format.default_print_language || + this.frm.doc.language || + frappe.boot.lang; + this.language_sel.val(this.lang_code); + } + + toggle_raw_printing() { + const is_raw_printing = this.is_raw_printing(); + this.wrapper.find('.btn-print-preview').toggle(!is_raw_printing); + this.wrapper.find('.btn-download-pdf').toggle(!is_raw_printing); + } + + preview() { + const $print_format = this.print_wrapper.find('iframe'); + this.$print_format_body = $print_format.contents(); + this.get_print_html((out) => { + if (!out.html) { + out.html = this.get_no_preview_html(); + } + + this.setup_print_format_dom(out, $print_format); + + const print_height = $print_format.get(0).offsetHeight; + const $message = this.wrapper.find('.page-break-message'); + + const print_height_inches = frappe.dom.pixel_to_inches(print_height); + // if contents are large enough, indicate that it will get printed on multiple pages + // Maximum height for an A4 document is 11.69 inches + if (print_height_inches > 11.69) { + $message.text(__('This may get printed on multiple pages')); + } else { + $message.text(''); + } + }); + } + + setup_print_format_dom(out, $print_format) { + this.print_wrapper.find('.print-format-skeleton').remove(); + this.$print_format_body.find('head').html( + ` + ` + ); + + if (frappe.utils.is_rtl(this.lang_code)) { + this.$print_format_body.find('head').append( + `` + ); + } + + this.$print_format_body.find('body').html( + `` + ); + + this.show_footer(); + + this.$print_format_body.find('.print-format').css({ + display: 'flex', + flexDirection: 'column', + }); + + this.$print_format_body.find('.page-break').css({ + display: 'flex', + 'flex-direction': 'column', + flex: '1', + }); + + setTimeout(() => { + $print_format.height(this.$print_format_body.find('.print-format').outerHeight()); + }, 500); + } + + hide() { + if (this.frm.setup_done && this.frm.page.current_view_name === 'print') { + this.frm.page.set_view( + this.frm.page.previous_view_name === 'print' + ? 'main' + : this.frm.page.previous_view_name || 'main' + ); + } + } + + show_footer() { + // footer is hidden by default as reqd by pdf generation + // simple hack to show it in print preview + + this.$print_format_body.find('#footer-html').attr( + 'style', + ` + display: block !important; + order: 1; + margin-top: auto; + padding-top: var(--padding-xl) + ` + ); + } + + printit() { + let me = this; + frappe.call({ + method: + 'frappe.printing.doctype.print_settings.print_settings.is_print_server_enabled', + callback: function(data) { + if (data.message) { + frappe.call({ + method: 'frappe.utils.print_format.print_by_server', + args: { + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + print_format: me.selected_format(), + no_letterhead: me.with_letterhead(), + letterhead: this.get_letterhead(), + }, + callback: function() {}, + }); + } else if (me.get_mapped_printer().length === 1) { + // printer is already mapped in localstorage (applies for both raw and pdf ) + if (me.is_raw_printing()) { + me.get_raw_commands(function(out) { + frappe.ui.form + .qz_connect() + .then(function() { + let printer_map = me.get_mapped_printer()[0]; + let data = [out.raw_commands]; + let config = qz.configs.create(printer_map.printer); + return qz.print(config, data); + }) + .then(frappe.ui.form.qz_success) + .catch((err) => { + frappe.ui.form.qz_fail(err); + }); + }); + } else { + frappe.show_alert( + { + message: __('PDF printing via "Raw Print" is not supported.'), + subtitle: __( + 'Please remove the printer mapping in Printer Settings and try again.' + ), + indicator: 'info', + }, + 14 + ); + //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing. + } + } else if (me.is_raw_printing()) { + // printer not mapped in localstorage and the current print format is raw printing + frappe.show_alert( + { + message: __('Printer mapping not set.'), + subtitle: __( + 'Please set a printer mapping for this print format in the Printer Settings' + ), + indicator: 'warning', + }, + 14 + ); + me.printer_setting_dialog(); + } else { + me.render_page('/printview?', true); + } + }, + }); + } + + render_page(method, printit = false) { + let w = window.open( + frappe.urllib.get_full_url( + method + + 'doctype=' + + encodeURIComponent(this.frm.doc.doctype) + + '&name=' + + encodeURIComponent(this.frm.doc.name) + + (printit ? '&trigger_print=1' : '') + + '&format=' + + encodeURIComponent(this.selected_format()) + + '&no_letterhead=' + + (this.with_letterhead() ? '0' : '1') + + '&letterhead=' + + encodeURIComponent(this.get_letterhead()) + + '&settings=' + + encodeURIComponent(JSON.stringify(this.additional_settings)) + + (this.lang_code ? '&_lang=' + this.lang_code : '') + ) + ); + if (!w) { + frappe.msgprint(__('Please enable pop-ups')); + return; + } + } + + get_print_html(callback) { + let print_format = this.get_print_format(); + if (print_format.raw_printing) { + callback({ + html: this.get_no_preview_html(), + }); + return; + } + if (this._req) { + this._req.abort(); + } + this._req = frappe.call({ + method: 'frappe.www.printview.get_html_and_style', + args: { + doc: this.frm.doc, + print_format: this.selected_format(), + no_letterhead: !this.with_letterhead() ? 1 : 0, + letterhead: this.get_letterhead(), + settings: this.additional_settings, + _lang: this.lang_code, + }, + callback: function(r) { + if (!r.exc) { + callback(r.message); + } + }, + }); + } + + get_letterhead() { + return this.letterhead_selector.val(); + } + + get_no_preview_html() { + return `
        + ${__('No Preview Available')} +
        `; + } + + get_raw_commands(callback) { + // fetches rendered raw commands from the server for the current print format. + frappe.call({ + method: 'frappe.www.printview.get_rendered_raw_commands', + args: { + doc: this.frm.doc, + print_format: this.selected_format(), + _lang: this.lang_code, + }, + callback: function(r) { + if (!r.exc) { + callback(r.message); + } + }, + }); + } + + get_mapped_printer() { + // returns a list of "print format: printer" mapping filtered by the current print format + let print_format_printer_map = this.get_print_format_printer_map(); + if (print_format_printer_map[this.frm.doctype]) { + return print_format_printer_map[this.frm.doctype].filter( + (printer_map) => printer_map.print_format == this.selected_format() + ); + } else { + return []; + } + } + + get_print_format_printer_map() { + // returns the whole object "print_format_printer_map" stored in the localStorage. + try { + let print_format_printer_map = JSON.parse( + localStorage.print_format_printer_map + ); + return print_format_printer_map; + } catch (e) { + return {}; + } + } + + refresh_print_options() { + this.print_formats = frappe.meta.get_print_formats(this.frm.doctype); + return this.print_sel.empty().add_options([ + this.get_default_option_for_select(__('Select Print Format')), + ...this.print_formats + ]); + } + + selected_format() { + return ( + this.print_sel.val() || this.frm.meta.default_print_format || 'Standard' + ); + } + + is_raw_printing(format) { + return this.get_print_format(format).raw_printing === 1; + } + + get_print_format(format) { + let print_format = {}; + if (!format) { + format = this.selected_format(); + } + + if (locals['Print Format'] && locals['Print Format'][format]) { + print_format = locals['Print Format'][format]; + } + + return print_format; + } + + with_letterhead() { + return cint(this.get_letterhead() !== __('No Letterhead')); + } + + set_style(style) { + frappe.dom.set_style(style || frappe.boot.print_css, 'print-style'); + } + + printer_setting_dialog() { + // dialog for the Printer Settings + this.print_format_printer_map = this.get_print_format_printer_map(); + this.data = this.print_format_printer_map[this.frm.doctype] || []; + this.printer_list = []; + frappe.ui.form.qz_get_printer_list().then((data) => { + this.printer_list = data; + const dialog = new frappe.ui.Dialog({ + title: __('Printer Settings'), + fields: [ + { + fieldtype: 'Section Break', + }, + { + fieldname: 'printer_mapping', + fieldtype: 'Table', + label: __('Printer Mapping'), + in_place_edit: true, + data: this.data, + get_data: () => { + return this.data; + }, + fields: [ + { + fieldtype: 'Select', + fieldname: 'print_format', + default: 0, + options: this.print_formats, + read_only: 0, + in_list_view: 1, + label: __('Print Format'), + }, + { + fieldtype: 'Select', + fieldname: 'printer', + default: 0, + options: this.printer_list, + read_only: 0, + in_list_view: 1, + label: __('Printer'), + }, + ], + }, + ], + primary_action: () => { + let printer_mapping = dialog.get_values()['printer_mapping']; + if (printer_mapping && printer_mapping.length) { + let print_format_list = printer_mapping.map((a) => a.print_format); + let has_duplicate = print_format_list.some( + (item, idx) => print_format_list.indexOf(item) != idx + ); + if (has_duplicate) + frappe.throw( + __( + 'Cannot have multiple printers mapped to a single print format.' + ) + ); + } else { + printer_mapping = []; + } + dialog.print_format_printer_map = this.get_print_format_printer_map(); + dialog.print_format_printer_map[this.frm.doctype] = printer_mapping; + localStorage.print_format_printer_map = JSON.stringify( + dialog.print_format_printer_map + ); + dialog.hide(); + }, + primary_action_label: __('Save'), + }); + dialog.show(); + if (!(this.printer_list && this.printer_list.length)) { + frappe.throw(__('No Printer is Available.')); + } + }); + } +}; diff --git a/frappe/printing/page/print/print.json b/frappe/printing/page/print/print.json new file mode 100644 index 0000000000..bea659c264 --- /dev/null +++ b/frappe/printing/page/print/print.json @@ -0,0 +1,18 @@ +{ + "content": null, + "creation": "2020-10-09 17:23:15.163030", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-10-09 17:23:15.163030", + "modified_by": "Administrator", + "module": "Printing", + "name": "print", + "owner": "Administrator", + "page_name": "Print", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0 +} \ No newline at end of file diff --git a/frappe/printing/page/print/print.py b/frappe/printing/page/print/print.py new file mode 100644 index 0000000000..7ddf68c30d --- /dev/null +++ b/frappe/printing/page/print/print.py @@ -0,0 +1,19 @@ +import frappe + +@frappe.whitelist() +def get_print_settings_to_show(doctype, docname): + doc = frappe.get_doc(doctype, docname) + print_settings = frappe.get_single('Print Settings') + + if hasattr(doc, 'get_print_settings'): + fields = doc.get_print_settings() or [] + else: + return [] + + print_settings_fields = [] + for fieldname in fields: + df = print_settings.meta.get_field(fieldname) + df.default = print_settings.get(fieldname) + print_settings_fields.append(df) + + return print_settings_fields diff --git a/frappe/printing/page/print/print_skeleton_loading.html b/frappe/printing/page/print/print_skeleton_loading.html new file mode 100644 index 0000000000..c1e6a0ddf4 --- /dev/null +++ b/frappe/printing/page/print/print_skeleton_loading.html @@ -0,0 +1,164 @@ + diff --git a/frappe/printing/page/print_format_builder/print_format_builder.css b/frappe/printing/page/print_format_builder/print_format_builder.css index 507cb520ed..0f7796536d 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.css +++ b/frappe/printing/page/print_format_builder/print_format_builder.css @@ -1,83 +1,160 @@ -.print-format-builder-section, .print-format-builder-add-section { - border: 1px solid #d1d8dd; +.print-format-builder-section { + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius); margin: 0px; - margin-bottom: 15px; + margin-bottom: var(--margin-md); } + .print-format-builder-add-section, .print-format-builder-header { - border: 1px dashed #d1d8dd; - padding: 15px; + border: 1px dashed var(--dark-border-color); + border-radius: var(--border-radius); + padding: var(--padding-sm); + width: 100%; + display: inline-block; + background: var(--bg-color); cursor: pointer; } +.print-format-builder-header-edit { + margin-bottom: var(--margin-sm); +} + .print-format-builder-header { - margin-bottom: 15px; + margin-bottom: var(--margin-md); +} + +.print-format-builder-add-section { + color: var(--text-light); + align-items: center; + padding: var(--padding-lg); + display: flex; + justify-content: center; +} + +.print-format-builder-add-section .icon { + margin-right: var(--margin-sm); } .print-format-builder-column { - padding: 15px; - margin: 0px -15px 0 -16px; - background-color: white; - min-height: 64px; - border-left: 1px solid #d1d8dd; - border-right: 1px solid #d1d8dd; + border-radius: var(--border-radius); } -.section-column:last-child .print-format-builder-column { - margin-right: -16px; +.print-format-builder-section .section-column { + padding: var(--padding-xs) 0 var(--padding-md) var(--padding-md); +} + +.print-format-builder-section .section-column:last-child { + padding-right: var(--padding-md); } .print-format-builder-field { - padding: 5px; - border: 1px solid #d1d8dd; - border-radius: 3px; - margin-bottom: 10px; - min-height: 34px; + padding: 8px; + width: 100%; + display: inline-block; + border-radius: var(--border-radius); + background: var(--bg-light-gray); + margin-bottom: var(--margin-sm); + font-size: var(--text-md); + color: var(--text-color); } .print-format-builder-field:last-child { - margin-bottom: 0px; + margin-bottom: 0; } -.print-format-builder-field-placeholder { - margin-bottom: 10px; -} - -.print-format-builder-field-placeholder:last-child { - margin-bottom: 0px; +.print-format-builder-field .field-label { + vertical-align: middle; } .print-format-builder-column .print-format-builder-field { cursor: move; } +.print-format-builder-section-head .section-label { + font-size: var(--text-lg); + color: var(--text-color); + font-weight: 500; + vertical-align: middle; + margin-left: var(--margin-sm); +} .print-format-builder-section-head { cursor: move; - padding: 10px 15px; - border-bottom: 1px solid #d1d8dd; + padding: var(--padding-md) calc(var(--padding-md) + 8px) + var(--padding-sm) calc(var(--padding-md) + 8px); +} + +.column-selector-row { + margin-bottom: var(--margin-xs); + padding: var(--padding-xs) 0; + cursor: grab; } .column-selector-row:hover { - background-color: #fafbfc; + background-color: var(--highlight-color); } -.column-selector-row .form-control { - margin-top: 5px; +.column-selector-row .drag-handle { + margin-right: var(--margin-sm); +} + +.print-format-builder-field .drag-handle { + margin-right: var(--margin-sm); +} + +.print-format-builder-sidebar .sidebar-field { + width: 100%; + padding: 4px 8px; + color: var(--text-on-light-gray); + /* color: var(--text-light); */ + text-align: left; + cursor: grab; +} + +.print-format-builder-sidebar .sidebar-custom-field { + background-color: var(--gray-300); } .print-format-builder-sidebar { - display: inline-block; - width: 160px; - vertical-align: top; - position: fixed; - top: 110px; - bottom: 0px; + top: calc(var(--navbar-height) + 70px); + position: sticky; +} + +.print-format-builder-sidebar-fields { + padding: var(--padding-xs); overflow-y: auto; + height: 75vh; +} + +.print-format-builder-field-placeholder { + margin-bottom: var(--margin-sm); +} + +.print-format-builder-field-placeholder:last-child { + margin-bottom: 0; +} + +.print-format-builder-field-placeholder .drag-handle { + margin-right: var(--margin-sm); +} + +.filter-searchbox { + padding: 0 var(--padding-xs); + margin-bottom: var(--margin-sm); +} + +.filter-searchbox input { + background-color: var(--control-bg-on-gray); } .print-format-builder-main { display: inline-block; vertical-align: top; border-top: 0px; - margin-left: 160px; + padding: var(--padding-lg); +} + +.print-format-help-message { + font-size: var(--text-md); + margin-bottom: var(--margin-md); } diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 4e049d120a..eb87190ab5 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -42,14 +42,12 @@ frappe.PrintFormatBuilder = Class.extend({ this.page = frappe.ui.make_app_page({ parent: this.parent, title: __("Print Format Builder"), - single_column: true }); this.page.main.css({"border-color": "transparent"}); - this.page.sidebar = $('').appendTo(this.page.main); - this.page.main = $('').appendTo(this.page.main); + this.page.sidebar = $('').appendTo(this.page.sidebar); + this.page.main = $('').appendTo(this.page.main); // future-bindings for buttons on sections / fields // bind only once @@ -61,7 +59,6 @@ frappe.PrintFormatBuilder = Class.extend({ }, show_start: function() { this.page.main.html(frappe.render_template("print_format_builder_start", {})); - this.page.sidebar.html(""); this.page.clear_actions(); this.page.set_title(__("Print Format Builder")); this.start_edit_print_format(); @@ -168,16 +165,11 @@ frappe.PrintFormatBuilder = Class.extend({ }); }, setup_sidebar: function() { - var me = this; - this.page.sidebar.empty(); - // prepend custom HTML field var fields = [this.get_custom_html_field()].concat(this.meta.fields); - - $(frappe.render_template("print_format_builder_sidebar", - {fields: fields})) - .appendTo(this.page.sidebar); - + this.page.sidebar.html( + $(frappe.render_template("print_format_builder_sidebar", {fields: fields})) + ); this.setup_field_filter(); }, get_custom_html_field: function() { @@ -216,8 +208,8 @@ frappe.PrintFormatBuilder = Class.extend({ if(!this.print_heading_template) { // default print heading template this.print_heading_template = ''; } @@ -319,7 +311,7 @@ frappe.PrintFormatBuilder = Class.extend({ var me = this; // drag from fields library - Sortable.create(this.page.sidebar.find(".print-format-builder-fields").get(0), + Sortable.create(this.page.sidebar.find(".print-format-builder-sidebar-fields").get(0), { group: { name:'field', put: true, pull:"clone" @@ -466,9 +458,6 @@ frappe.PrintFormatBuilder = Class.extend({ field.remove(); }, input_class: "btn-danger", - input_css: { - "margin-top": "10px" - } } ], }); diff --git a/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html b/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html index 697f27866e..15d029704b 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html @@ -1,33 +1,36 @@ -

        {{ __("Check columns to select, drag to set order.") }} +

        {{ __("Check columns to select, drag to set order.") }} {{ __("Widths can be set in px or %.") }}

        {{ __("Some columns might get cut off when printing to PDF. Try to keep number of columns under 10.") }}

        -

        {{ __("Column") }}

        -

        {{ __("Width") }}

        +

        {{ __("Column") }}

        +

        {{ __("Width") }}

        - {% for (i=0; i < fields.length; i++) { var f = fields[i]; %} - {% var selected = in_list(column_names, f.fieldname) %} -
        -
        -
        - -
        -
        -
        - -
        -
        - {% } %} + {% for (i=0; i < fields.length; i++) { var f = fields[i]; %} + {% var selected = in_list(column_names, f.fieldname) %} +
        +
        +
        + +
        +
        + +
        +
        +
        + +
        +
        + {% } %}
        diff --git a/frappe/printing/page/print_format_builder/print_format_builder_field.html b/frappe/printing/page/print_format_builder/print_format_builder_field.html index eb29092f28..0e207222a5 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_field.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_field.html @@ -1,35 +1,46 @@ diff --git a/frappe/printing/page/print_format_builder/print_format_builder_layout.html b/frappe/printing/page/print_format_builder/print_format_builder_layout.html index 04475d8fab..f45a760cbc 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_layout.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_layout.html @@ -1,11 +1,15 @@ -
        -
        - {%= __("Drag elements from the sidebar to add. Drag them back to trash.") %}

        +
        +