Browse Source

Merge branch 'develop' into patch-1

version-14
Prssanna Desai 4 years ago
committed by GitHub
parent
commit
4b73a7f2fb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1376 additions and 2021 deletions
  1. +1
    -0
      .eslintrc
  2. +1
    -1
      .github/CONTRIBUTING.md
  3. +5
    -5
      .github/helper/translation.py
  4. +2
    -2
      .gitignore
  5. +9
    -8
      .travis.yml
  6. +5
    -5
      CODEOWNERS
  7. +5
    -1
      cypress.json
  8. +3
    -3
      cypress/integration/api.js
  9. +6
    -6
      cypress/integration/awesome_bar.js
  10. +1
    -1
      cypress/integration/control_barcode.js
  11. +5
    -5
      cypress/integration/control_duration.js
  12. +7
    -10
      cypress/integration/control_link.js
  13. +1
    -1
      cypress/integration/control_rating.js
  14. +2
    -2
      cypress/integration/datetime.js
  15. +3
    -3
      cypress/integration/depends_on.js
  16. +15
    -23
      cypress/integration/file_uploader.js
  17. +17
    -23
      cypress/integration/form.js
  18. +8
    -8
      cypress/integration/grid_pagination.js
  19. +10
    -9
      cypress/integration/list_view.js
  20. +16
    -16
      cypress/integration/list_view_settings.js
  21. +8
    -8
      cypress/integration/login.js
  22. +15
    -15
      cypress/integration/query_report.js
  23. +21
    -25
      cypress/integration/recorder.js
  24. +15
    -17
      cypress/integration/relative_time_filters.js
  25. +4
    -5
      cypress/integration/report_view.js
  26. +5
    -6
      cypress/integration/table_multiselect.js
  27. +25
    -8
      cypress/support/commands.js
  28. +1
    -1
      cypress/support/index.js
  29. +9
    -0
      frappe/.stylelintrc
  30. +12
    -9
      frappe/__init__.py
  31. +1
    -1
      frappe/auth.py
  32. +0
    -70
      frappe/automation/desk_page/tools/tools.json
  33. +4
    -3
      frappe/automation/doctype/auto_repeat/auto_repeat.js
  34. +229
    -0
      frappe/automation/workspace/tools/tools.json
  35. +37
    -21
      frappe/boot.py
  36. +5
    -10
      frappe/cache_manager.py
  37. +3
    -2
      frappe/commands/utils.py
  38. +1
    -1
      frappe/config/__init__.py
  39. +0
    -59
      frappe/config/automation.py
  40. +0
    -66
      frappe/config/core.py
  41. +0
    -60
      frappe/config/customization.py
  42. +0
    -67
      frappe/config/desk.py
  43. +0
    -133
      frappe/config/desktop.py
  44. +0
    -6
      frappe/config/docs.py
  45. +0
    -127
      frappe/config/integrations.py
  46. +0
    -195
      frappe/config/settings.py
  47. +0
    -2
      frappe/config/tools.py
  48. +0
    -85
      frappe/config/users_and_permissions.py
  49. +0
    -117
      frappe/config/website.py
  50. +3
    -2
      frappe/contacts/doctype/contact/contact.json
  51. +0
    -74
      frappe/core/desk_page/settings/settings.json
  52. +0
    -59
      frappe/core/desk_page/users/users.json
  53. +22
    -7
      frappe/core/doctype/comment/comment.py
  54. +5
    -4
      frappe/core/doctype/communication/communication.js
  55. +10
    -11
      frappe/core/doctype/communication/communication.py
  56. +1
    -1
      frappe/core/doctype/data_import_legacy/data_import_legacy.js
  57. +6
    -5
      frappe/core/doctype/deleted_document/deleted_document_list.js
  58. +2
    -2
      frappe/core/doctype/doctype/doctype.js
  59. +4
    -3
      frappe/core/doctype/doctype/doctype.json
  60. +35
    -48
      frappe/core/doctype/doctype/doctype.py
  61. +7
    -0
      frappe/core/doctype/doctype/patches/set_route.py
  62. +11
    -6
      frappe/core/doctype/doctype/test_doctype.py
  63. +32
    -9
      frappe/core/doctype/file/file.py
  64. +1
    -1
      frappe/core/doctype/log_settings/log_settings.py
  65. +1
    -1
      frappe/core/doctype/module_def/module_def.json
  66. +0
    -0
      frappe/core/doctype/module_profile/__init__.py
  67. +19
    -0
      frappe/core/doctype/module_profile/module_profile.js
  68. +60
    -0
      frappe/core/doctype/module_profile/module_profile.json
  69. +12
    -0
      frappe/core/doctype/module_profile/module_profile.py
  70. +32
    -0
      frappe/core/doctype/module_profile/test_module_profile.py
  71. +1
    -1
      frappe/core/doctype/navbar_settings/navbar_settings.py
  72. +3
    -0
      frappe/core/doctype/page/page.py
  73. +5
    -0
      frappe/core/doctype/page/patches/drop_unused_pages.py
  74. +3
    -1
      frappe/core/doctype/page/test_page.py
  75. +10
    -0
      frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
  76. +84
    -2
      frappe/core/doctype/role/role.json
  77. +25
    -5
      frappe/core/doctype/role/role.py
  78. +14
    -14
      frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js
  79. +6
    -8
      frappe/core/doctype/role_profile/role_profile.js
  80. +1
    -1
      frappe/core/doctype/server_script/test_server_script.py
  81. +15
    -1
      frappe/core/doctype/system_settings/system_settings.json
  82. +6
    -1
      frappe/core/doctype/user/test_user.py
  83. +0
    -28
      frappe/core/doctype/user/user.css
  84. +24
    -45
      frappe/core/doctype/user/user.js
  85. +51
    -31
      frappe/core/doctype/user/user.json
  86. +50
    -25
      frappe/core/doctype/user/user.py
  87. +49
    -3
      frappe/core/doctype/user_permission/test_user_permission.py
  88. +11
    -2
      frappe/core/doctype/user_permission/user_permission.js
  89. +37
    -251
      frappe/core/doctype/user_permission/user_permission.json
  90. +20
    -17
      frappe/core/doctype/user_permission/user_permission.py
  91. +25
    -2
      frappe/core/doctype/user_permission/user_permission_list.js
  92. +60
    -0
      frappe/core/page/background_jobs/background_jobs.css
  93. +25
    -14
      frappe/core/page/background_jobs/background_jobs.html
  94. +60
    -40
      frappe/core/page/background_jobs/background_jobs.js
  95. +1
    -8
      frappe/core/page/background_jobs/background_jobs_outer.html
  96. +0
    -0
      frappe/core/page/dashboard_view/__init__.py
  97. +7
    -7
      frappe/core/page/dashboard_view/dashboard_view.js
  98. +3
    -3
      frappe/core/page/dashboard_view/dashboard_view.json
  99. +0
    -3
      frappe/core/page/desktop/desktop.js
  100. +0
    -24
      frappe/core/page/desktop/desktop.json

+ 1
- 0
.eslintrc View File

@@ -87,6 +87,7 @@
"open_url_post": true,
"toTitle": true,
"lstrip": true,
"rstrip": true,
"strip": true,
"strip_html": true,
"replace_all": true,


+ 1
- 1
.github/CONTRIBUTING.md View File

@@ -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


+ 5
- 5
.github/helper/translation.py View File

@@ -2,7 +2,7 @@ import re
import sys

errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\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


+ 2
- 2
.gitignore View File

@@ -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/


+ 9
- 8
.travis.yml View File

@@ -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:


+ 5
- 5
CODEOWNERS View File

@@ -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


+ 5
- 1
cypress.json View File

@@ -3,5 +3,9 @@
"projectId": "92odwv",
"adminPassword": "admin",
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000
"pageLoadTimeout": 15000,
"retries": {
"runMode": 2,
"openMode": 2
}
}

+ 3
- 3
cypress/integration/api.js View File

@@ -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', () => {


+ 6
- 6
cypress/integration/awesome_bar.js View File

@@ -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', () => {


+ 1
- 1
cypress/integration/control_barcode.js View File

@@ -1,7 +1,7 @@
context('Control Barcode', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});

function get_dialog_with_barcode() {


+ 5
- 5
cypress/integration/control_duration.js View File

@@ -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');


+ 7
- 10
cypress/integration/control_link.js View File

@@ -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]}`);
});
});
});

+ 1
- 1
cypress/integration/control_rating.js View File

@@ -1,7 +1,7 @@
context('Control Rating', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});

function get_dialog_with_rating() {


+ 2
- 2
cypress/integration/datetime.js View File

@@ -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')


+ 3
- 3
cypress/integration/depends_on.js View File

@@ -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');


+ 15
- 23
cypress/integration/file_uploader.js View File

@@ -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');


+ 17
- 23
cypress/integration/form.js View File

@@ -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();


+ 8
- 8
cypress/integration/grid_pagination.js View File

@@ -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();


+ 10
- 9
cypress/integration/list_view.js View File

@@ -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');
});
});


+ 16
- 16
cypress/integration/list_view_settings.js View File

@@ -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();
});
});

+ 8
- 8
cypress/integration/login.js View File

@@ -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'));


+ 15
- 15
cypress/integration/query_report.js View File

@@ -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 });
});
});
});

+ 21
- 25
cypress/integration/recorder.js View File

@@ -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();
});
});

+ 15
- 17
cypress/integration/relative_time_filters.js View File

@@ -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');
});
});

+ 4
- 5
cypress/integration/report_view.js View File

@@ -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 => {


+ 5
- 6
cypress/integration/table_multiselect.js View File

@@ -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');
});
});

+ 25
- 8
cypress/support/commands.js View File

@@ -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(200);
cy.get_open_dialog().find('.btn-modal-close').click();
cy.get('.modal:visible').should('not.exist');
});

@@ -307,4 +306,22 @@ Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
return res.body.data;
});
});
});
});

Cypress.Commands.add('add_filter', () => {
cy.get('.filter-section .filter-button').click();
cy.wait(300);
cy.get('.filter-popover').should('exist');
cy.get('.filter-popover').find('.add-filter').click();
});

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();
});
});

+ 1
- 1
cypress/support/index.js View File

@@ -21,5 +21,5 @@ import './commands';
// require('./commands')

Cypress.Cookies.defaults({
whitelist: 'sid'
preserve: 'sid'
});

+ 9
- 0
frappe/.stylelintrc View File

@@ -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
}
}

+ 12
- 9
frappe/__init__.py View File

@@ -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 = '<a href="#Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
html = '<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
return html.format(
doctype=doctype,
name=name,


+ 1
- 1
frappe/auth.py View File

@@ -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


+ 0
- 70
frappe/automation/desk_page/tools/tools.json View File

@@ -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"
}
]
}

+ 4
- 3
frappe/automation/doctype/auto_repeat/auto_repeat.js View File

@@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
refresh: function(frm) {
// auto repeat message
if (frm.is_new()) {
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
let customize_form_link = `<a href="/app/customize form">${__('Customize Form')}</a>`;
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();
});


+ 229
- 0
frappe/automation/workspace/tools/tools.json View File

@@ -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"
}
]
}

+ 37
- 21
frappe/boot.py View File

@@ -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})]
@@ -88,6 +89,7 @@ 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()

return bootinfo

@@ -106,11 +108,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 +222,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 +250,12 @@ def add_home_page(bootinfo, docs):

try:
page = frappe.desk.desk_page.get(home_page)
docs.append(page)
bootinfo['home_page'] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError):
if frappe.message_log:
frappe.message_log.pop()
page = frappe.desk.desk_page.get('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 +271,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 +306,21 @@ 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

+ 5
- 10
frappe/cache_manager.py View File

@@ -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)



+ 3
- 2
frappe/commands/utils.py View File

@@ -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'


+ 1
- 1
frappe/config/__init__.py View File

@@ -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"

+ 0
- 59
frappe/config/automation.py View File

@@ -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

+ 0
- 66
frappe/config/core.py View File

@@ -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"),
},
]
}
]

+ 0
- 60
frappe/config/customization.py View File

@@ -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")
}
]
}
]

+ 0
- 67
frappe/config/desk.py View File

@@ -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"),
},
]
}
]

+ 0
- 133
frappe/config/desktop.py View File

@@ -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
},
]

+ 0
- 6
frappe/config/docs.py View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

source_link = "https://github.com/frappe/frappe_io"
docs_base_url = "/docs"

+ 0
- 127
frappe/config/integrations.py View File

@@ -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."),
}
]
}
]

+ 0
- 195
frappe/config/settings.py View File

@@ -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

+ 0
- 2
frappe/config/tools.py View File

@@ -1,2 +0,0 @@
from __future__ import unicode_literals
from frappe import _

+ 0
- 85
frappe/config/users_and_permissions.py View File

@@ -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")
}
]
}
]

+ 0
- 117
frappe/config/website.py View File

@@ -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",
},
]
},

]

+ 3
- 2
frappe/contacts/doctype/contact/contact.json View File

@@ -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",


+ 0
- 74
frappe/core/desk_page/settings/settings.json View File

@@ -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"
}

+ 0
- 59
frappe/core/desk_page/users/users.json View File

@@ -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"
}
]
}

+ 22
- 7
frappe/core/doctype/comment/comment.py View File

@@ -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)


+ 5
- 4
frappe/core/doctype/communication/communication.js View File

@@ -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'
});
}
);
}


+ 10
- 11
frappe/core/doctype/communication/communication.py View File

@@ -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()


+ 1
- 1
frappe/core/doctype/data_import_legacy/data_import_legacy.js View File

@@ -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 + "%");


+ 6
- 5
frappe/core/doctype/deleted_document/deleted_document_list.js View File

@@ -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 `<li><a href='/desk#Form/Deleted Document/${docname}'>${docname}</a></li>`;
return `<li><a href='/app/deleted-document/${docname}'>${docname}</a></li>`;
});
return "<br><ul>" + html.join("");
}
function message(title, docnames) {
};

let message = (title, docnames) => {
return (docnames.length > 0) ? title + body(docnames) + "</ul>": "";
}
};

const { restored, invalid, failed } = r.message;
const restored_summary = message(__("Documents restored successfully"), restored);


+ 2
- 2
frappe/core/doctype/doctype/doctype.js View File

@@ -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)}`);
});
}
}


+ 4
- 3
frappe/core/doctype/doctype/doctype.json View File

@@ -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"
@@ -609,7 +609,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2020-09-24 13:13:58.227153",
"modified": "2020-12-10 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",


+ 35
- 48
frappe/core/doctype/doctype/doctype.py View File

@@ -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,15 +63,7 @@ class DocType(Document):

self.validate_name()

if self.issingle:
self.allow_import = 0
self.is_submittable = 0
self.istable = 0

elif self.istable:
self.allow_import = 0
self.permissions = []

self.set_defaults_for_single_and_table()
self.scrub_field_names()
self.set_default_in_list_view()
self.set_default_translatable()
@@ -79,22 +71,17 @@ class DocType(Document):
self.validate_document_type()
validate_fields(self)

if self.istable:
# no permission records for child table
self.permissions = []
else:
if not self.istable:
validate_permissions(self)

self.make_amendable()
self.make_repeatable()
self.validate_nestedset()
self.validate_website()
self.validate_links_table_fieldnames()
validate_links_table_fieldnames(self)

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)
@@ -102,14 +89,20 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))

if 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_defaults_for_single_and_table(self):
if self.issingle:
self.allow_import = 0
self.is_submittable = 0
self.istable = 0

elif self.istable:
self.allow_import = 0
self.permissions = []

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
@@ -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):


+ 7
- 0
frappe/core/doctype/doctype/patches/set_route.py View File

@@ -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)

+ 11
- 6
frappe/core/doctype/doctype/test_doctype.py View File

@@ -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):


+ 32
- 9
frappe/core/doctype/file/file.py View File

@@ -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):


+ 1
- 1
frappe/core/doctype/log_settings/log_settings.py View File

@@ -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('<a href="/desk#List/Error%20Log/List"> Error Logs </a>')
'message': _("You have unseen {0}").format('<a href="/app/List/Error%20Log/List"> Error Logs </a>')
}

if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):


+ 1
- 1
frappe/core/doctype/module_def/module_def.json View File

@@ -51,7 +51,7 @@
"link_fieldname": "module"
},
{
"link_doctype": "Desk Page",
"link_doctype": "Workspace",
"link_fieldname": "module"
}
],


frappe/core/page/dashboard/__init__.py → frappe/core/doctype/module_profile/__init__.py View File


+ 19
- 0
frappe/core/doctype/module_profile/module_profile.js View File

@@ -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 = $('<div style="min-height: 300px">')
.appendTo(frm.fields_dict.module_html.wrapper);

frm.module_editor = new frappe.ModuleEditor(frm, module_area);
}
}

if (frm.module_editor) {
frm.module_editor.refresh();
}
}
});

+ 60
- 0
frappe/core/doctype/module_profile/module_profile.json View File

@@ -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
}

+ 12
- 0
frappe/core/doctype/module_profile/module_profile.py View File

@@ -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()])

+ 32
- 0
frappe/core/doctype/module_profile/test_module_profile.py View File

@@ -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')

+ 1
- 1
frappe/core/doctype/navbar_settings/navbar_settings.py View File

@@ -23,7 +23,7 @@ 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')
if not app_logo:


+ 3
- 0
frappe/core/doctype/page/page.py View File

@@ -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"))



+ 5
- 0
frappe/core/doctype/page/patches/drop_unused_pages.py View File

@@ -0,0 +1,5 @@
import frappe

def execute():
for name in ('desktop', 'space'):
frappe.delete_doc('Page', name)

+ 3
- 1
frappe/core/doctype/page/test_page.py View File

@@ -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)

+ 10
- 0
frappe/core/doctype/role/patches/v13_set_default_desk_properties.py View File

@@ -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()

+ 84
- 2
frappe/core/doctype/role/role.json View File

@@ -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",


+ 25
- 5
frappe/core/doctype/role/role.py View File

@@ -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'''


+ 14
- 14
frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.js View File

@@ -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 = $('<div style="min-height: 300px">')
.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");


+ 6
- 8
frappe/core/doctype/role_profile/role_profile.js View File

@@ -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 = $('<div style="min-height: 300px">')
.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();
}
}


+ 1
- 1
frappe/core/doctype/server_script/test_server_script.py View File

@@ -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')


+ 15
- 1
frappe/core/doctype/system_settings/system_settings.json View File

@@ -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",


+ 6
- 1
frappe/core/doctype/user/test_user.py View File

@@ -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):


+ 0
- 28
frappe/core/doctype/user/user.css View File

@@ -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%;
}

+ 24
- 45
frappe/core/doctype/user/user.js View File

@@ -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 = $('<div style="min-height: 300px">')
if (frm.can_edit_roles && !frm.is_new()) {
if (!frm.roles_editor) {
const role_area = $('<div class="role-editor">')
.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 = $('<div style="min-height: 300px">')
var module_area = $('<div>')
.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 = $('<div class="row module-block-list"></div>').appendTo(wrapper);
this.frm = frm;
this.make();
},
make: function() {
var me = this;
this.frm.doc.__onload.all_modules.forEach(function(m) {
$(repl('<div class="col-sm-6"><div class="checkbox">\
<label><input type="checkbox" class="block-module-check" data-module="%(module)s">\
%(module)s</label></div></div>', {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});
}
});
}
});

+ 51
- 31
frappe/core/doctype/user/user.json View File

@@ -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",
"restrict_ip",
"last_ip",
"column_break1",
"login_after",
"user_type",
"last_active",
"section_break_63",
"login_before",
"restrict_ip",
"bypass_restrict_ip_check_if_2fa_enabled",
"column_break1",
"last_login",
"last_ip",
"last_active",
"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",
@@ -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-01-02 11:21:50.507786",
"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
}
}

+ 50
- 25
frappe/core/doctype/user/user.py View File

@@ -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='<p>Your verification code is {0}</p>'.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


+ 49
- 3
frappe/core/doctype/user_permission/test_user_permission.py View File

@@ -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})


+ 11
- 2
frappe/core/doctype/user_permission/user_permission.js View File

@@ -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);
}




+ 37
- 251
frappe/core/doctype/user_permission/user_permission.json View File

@@ -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 <b>For Value</b>.",
"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
}

+ 20
- 17
frappe/core/doctype/user_permission/user_permission.py View File

@@ -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


+ 25
- 2
frappe/core/doctype/user_permission/user_permission_list.js View File

@@ -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();
}
};
};

+ 60
- 0
frappe/core/page/background_jobs/background_jobs.css View File

@@ -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);
}

+ 25
- 14
frappe/core/page/background_jobs/background_jobs.html View File

@@ -1,6 +1,6 @@
<div class="list-jobs">
{% if jobs.length %}
<table class="table table-bordered" style="table-layout: fixed;">
<table class="table table-borderless" style="table-layout: fixed;">
<thead>
<tr>
<th style="width: 20%">{{ __("Queue / Worker") }}</th>
@@ -11,30 +11,41 @@
<tbody>
{% for j in jobs %}
<tr>
<td><span class="indicator {{ j.color }}" title="{{ j.status }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td class="worker-name">
<span class="indicator-pill no-margin {{ j.color }}"></span>
<span class="ml-2">{{ j.queue.split(".").slice(-1)[0] }}</span>
</td>
<td style="overflow: auto;">
<div>
{{ frappe.utils.encode_tags(j.job_name) }}
<span class="job-name">
{{ frappe.utils.encode_tags(j.job_name) }}
</span>
</div>
{% if j.exc_info %}
<div>
<div class="exc_info">
<pre>{{ frappe.utils.encode_tags(j.exc_info) }}</pre>
</div>
{% endif %}
</td>
<td class="small">{{ j.creation }}</td>
<td class="creation">{{ j.creation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>
<span class="indicator blue" style="margin-right: 20px;">{{ __("Started") }}</span>
<span class="indicator orange" style="margin-right: 20px;">{{ __("Queued") }}</span>
<span class="indicator red" style="margin-right: 20px;">{{ __("Failed") }}</span>
<span class="indicator green">{{ __("Finished") }}</span>
</p>
{% else %}
<p class="text-muted">{{ __("No pending or current jobs for this site") }}</p>
<div class="no-background-jobs">
<img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Empty State">
<p class="text-muted">{{ __("No pending or current jobs for this site") }}</p>
</div>
{% endif %}
<p class="text-muted" style="margin-top: 30px;">{{ __("Last refreshed") }} {{ frappe.datetime.now_datetime() }}</p>
</div>
<div class="footer row">
<div class="col-md-6 text-muted text-center text-md-left">{{ __("Last refreshed") }}
{{ frappe.datetime.now_datetime(true).toLocaleString() }}</div>
<div class="col-md-6 text-center text-md-right">
<span class="indicator-pill blue" class="mr-2">{{ __("Started") }}</span>
<span class="indicator-pill orange" class="mr-2">{{ __("Queued") }}</span>
<span class="indicator-pill red" class="mr-2">{{ __("Failed") }}</span>
<span class="indicator-pill green">{{ __("Finished") }}</span>
</div>
</div>
</div>

+ 60
- 40
frappe/core/page/background_jobs/background_jobs.js View File

@@ -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);
}
});
}

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.called = false;
this.show_failed = false;

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);
}
}
});
}
}

+ 1
- 8
frappe/core/page/background_jobs/background_jobs_outer.html View File

@@ -1,11 +1,4 @@
<div style="padding: 20px;">
<p>
<div class="checkbox">
<label>
<input type="checkbox" class="show-failed"> {{ __("Show failed jobs") }}
</label>
</div>
</p>
<div class="frappe-card">
<div class="table-area">

</div>

frappe/core/page/desktop/__init__.py → frappe/core/page/dashboard_view/__init__.py View File


frappe/core/page/dashboard/dashboard.js → frappe/core/page/dashboard_view/dashboard_view.js View File

@@ -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);
$(`<div class="dashboard" style="overflow-y: hidden">
$(`<div class="dashboard" style="overflow: visible">
<div class="dashboard-graph"></div>
</div>`).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);
}
});
});

frappe/core/page/dashboard/dashboard.json → frappe/core/page/dashboard_view/dashboard_view.json View File

@@ -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",

+ 0
- 3
frappe/core/page/desktop/desktop.js View File

@@ -1,3 +0,0 @@
frappe.pages['desktop'].on_page_load = function() {
frappe.utils.set_title(__("Home"));
};

+ 0
- 24
frappe/core/page/desktop/desktop.json View File

@@ -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"
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save