Merge remote-tracking branch 'upstream/develop' into feat-camera-attach

This commit is contained in:
Faris Ansari 2021-02-08 13:25:15 +05:30
commit d7b531e63c
382 changed files with 6853 additions and 4196 deletions

View file

@ -1,2 +0,0 @@
exclude_paths:
- '**.sql'

View file

@ -1,17 +0,0 @@
version = 1
test_patterns = [
"**/test_*.py"
]
exclude_patterns = [
"frappe/patches/**",
"*.min.js"
]
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"

View file

@ -5,5 +5,4 @@ frappe/core/doctype/doctype/boilerplate/*
frappe/core/doctype/report/boilerplate/*
frappe/public/js/frappe/class.js
frappe/templates/includes/*
frappe/tests/testcafe/*
frappe/www/website_script.js
frappe/www/website_script.js

View file

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

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

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

4
.gitignore vendored
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
.stylelintrc Normal file
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
}
}

View file

@ -31,19 +31,18 @@ matrix:
- name: "Python 3.7 MariaDB"
python: 3.7
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
script: bench --verbose --site test_site run-tests --coverage
- name: "Python 3.7 PostgreSQL"
python: 3.7
env: DB=postgres TYPE=server
script: bench --site test_site run-tests --coverage
script: bench --verbose --site test_site run-tests --coverage
- name: "Cypress"
python: 3.7
env: DB=mariadb TYPE=ui
before_script:
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
- bench --site test_site_producer execute frappe.utils.install.complete_setup_wizard
script: bench --site test_site run-ui-tests frappe --headless
before_install:
@ -75,8 +74,10 @@ install:
- mkdir ~/frappe-bench/sites/test_site
- cp $TRAVIS_BUILD_DIR/.travis/consumer_db/$DB.json ~/frappe-bench/sites/test_site/site_config.json
- mkdir ~/frappe-bench/sites/test_site_producer
- cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json
- if [ $TYPE == "server" ]; then
mkdir ~/frappe-bench/sites/test_site_producer;
cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json;
fi
- if [ $DB == "mariadb" ];then
mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
@ -104,11 +105,11 @@ install:
- cd ./frappe-bench
- sed -i 's/watch:/# watch:/g' Procfile
- sed -i 's/schedule:/# schedule:/g' Procfile
- sed -i 's/^watch:/# watch:/g' Procfile
- sed -i 's/^schedule:/# schedule:/g' Procfile
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
@ -119,7 +120,7 @@ install:
- bench start &
- bench --site test_site reinstall --yes
- bench --site test_site_producer reinstall --yes
- if [ $TYPE == "server" ]; then bench --site test_site_producer reinstall --yes; fi
- bench build --app frappe
after_script:

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

View file

@ -1,11 +0,0 @@
#!/bin/bash
# stolen from http://cgit.drupalcode.org/octopus/commit/?id=db4f837
includedir=`mysql_config --variable=pkgincludedir`
thiscwd=`pwd`
_THIS_DB_VERSION=`mysql -V 2>&1 | tr -d "\n" | cut -d" " -f6 | awk '{ print $1}' | cut -d"-" -f1 | awk '{ print $1}' | sed "s/[\,']//g"`
if [ "$_THIS_DB_VERSION" = "5.5.40" ] && [ ! -e "$includedir-$_THIS_DB_VERSION-fixed.log" ] ; then
cd $includedir
sudo patch -p1 < $thiscwd/ci/my_config.h.patch &> /dev/null
sudo touch $includedir-$_THIS_DB_VERSION-fixed.log
fi

View file

@ -1,22 +0,0 @@
diff -burp a/my_config.h b/my_config.h
--- a/my_config.h 2014-10-09 19:32:46.000000000 -0400
+++ b/my_config.h 2014-10-09 19:35:12.000000000 -0400
@@ -641,17 +641,4 @@
#define SIZEOF_TIME_T 8
/* #undef TIME_T_UNSIGNED */
-/*
- stat structure (from <sys/stat.h>) is conditionally defined
- to have different layout and size depending on the defined macros.
- The correct macro is defined in my_config.h, which means it MUST be
- included first (or at least before <features.h> - so, practically,
- before including any system headers).
-
- __GLIBC__ is defined in <features.h>
-*/
-#ifdef __GLIBC__
-#error <my_config.h> MUST be included first!
-#endif
-
#endif

View file

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

View file

@ -2,7 +2,7 @@ context('API Resources', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
it('Creates two Comments', () => {

View file

@ -2,7 +2,7 @@ context('Awesome Bar', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
beforeEach(() => {

View file

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

View file

@ -1,7 +1,7 @@
context('Control Duration', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) {

View file

@ -1,11 +1,11 @@
context('Control Link', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
beforeEach(() => {
cy.visit('/app/space/Website');
cy.visit('/app/website');
cy.create_records({
doctype: 'ToDo',
description: 'this is a test todo for link'
@ -29,8 +29,7 @@ context('Control Link', () => {
it('should set the valid value', () => {
get_dialog_with_link().as('dialog');
cy.server();
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
cy.wait('@search_link');
@ -50,8 +49,7 @@ context('Control Link', () => {
it('should unset invalid value', () => {
get_dialog_with_link().as('dialog');
cy.server();
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
cy.get('.frappe-control[data-fieldname=link] input')
.type('invalid value', { delay: 100 })
@ -63,9 +61,8 @@ context('Control Link', () => {
it('should route to form on arrow click', () => {
get_dialog_with_link().as('dialog');
cy.server();
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('@todos').then(todos => {
cy.get('.frappe-control[data-fieldname=link] input').as('input');

View file

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

View file

@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name;
context('Control Date, Time and DateTime', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
return cy.insert_doc('DocType', datetime_doctype, true);
});
@ -42,7 +42,7 @@ context('Control Date, Time and DateTime', () => {
.should('be.visible');
cy.get(
'.datepickers-container .datepicker.active .datepicker--cell-day.-current-'
).click();
).click({ force: true });
cy.window()
.its('cur_frm')

View file

@ -1,9 +1,33 @@
context('Depends On', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
name: 'Child Test Depends On',
fields: [
{
"label": "Child Test Field",
"fieldname": "child_test_field",
"fieldtype": "Data",
"in_list_view": 1,
},
{
"label": "Child Dependant Field",
"fieldname": "child_dependant_field",
"fieldtype": "Data",
"in_list_view": 1,
},
{
"label": "Child Display Dependant Field",
"fieldname": "child_display_dependant_field",
"fieldtype": "Data",
"in_list_view": 1,
},
]
});
}).then(frappe => {
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Depends On',
fields: [
{
@ -24,6 +48,13 @@ context('Depends On', () => {
"fieldtype": "Data",
'depends_on': "eval:doc.test_field=='Value'"
},
{
"label": "Child Test Depends On Field",
"fieldname": "child_test_depends_on_field",
"fieldtype": "Table",
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
'options': "Child Test Depends On"
},
]
});
});
@ -48,6 +79,30 @@ context('Depends On', () => {
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
});
it('should set the table and its fields as read only depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('dependant_field', 'Some Value');
//cy.fill_field('test_field', 'Some Other Value');
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('@table').find('[data-idx="1"]').as('row1');
cy.get('@row1').find('.btn-open-row').click();
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
//cy.get('@row1-form_in_grid').find('')
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
cy.get('@row1-form_in_grid').find('.grid-collapse-row').click();
// set the table to read-only
cy.fill_field('test_field', 'Some Other Value');
// grid row form fields should be read-only
cy.get('@row1').find('.btn-open-row').click();
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled');
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled');
});
it('should display the field depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');

View file

@ -1,7 +1,7 @@
context('FileUploader', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app');
});
function open_upload_dialog() {
@ -14,40 +14,32 @@ context('FileUploader', () => {
open_upload_dialog();
cy.get_open_dialog().should('contain', 'Drag and drop files');
cy.hide_dialog();
cy.get('body').click();
});
it('should accept dropped files', () => {
open_upload_dialog();
cy.fixture('example.json').then(fileContent => {
cy.get_open_dialog().find('.file-upload-area').upload({
fileContent,
fileName: 'example.json',
mimeType: 'application/json'
}, {
subjectType: 'drag-n-drop',
force: true
});
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-modal-primary').click();
cy.wait('@upload_file').its('status').should('be', 200);
cy.get('.modal:visible').should('not.exist');
cy.get_open_dialog().find('.file-upload-area').attachFile('example.json', {
subjectType: 'drag-n-drop',
});
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-modal-primary').click();
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
cy.get('.modal:visible').should('not.exist');
});
it('should accept uploaded files', () => {
open_upload_dialog();
cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click();
cy.get('.file-filter').type('example.json');
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();
cy.wait('@upload_file').its('response.body.message')
.should('have.property', 'file_url', '/private/files/example.json');
.should('have.property', 'file_name', 'example.json');
cy.get('.modal:visible').should('not.exist');
});
@ -56,8 +48,7 @@ context('FileUploader', () => {
cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click();
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();
cy.wait('@upload_file').its('response.body.message')
.should('have.property', 'file_url', 'https://github.com');

View file

@ -1,7 +1,7 @@
context('Form', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
@ -11,13 +11,12 @@ context('Form', () => {
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
cy.wait(300);
cy.get('.page-title').should('contain', 'Not Saved');
cy.server();
cy.route({
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.form.save.savedocs'
}).as('form_save');
cy.get('.primary-action').click();
cy.wait('@form_save').its('status').should('eq', 200);
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
cy.visit('/app/todo');
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
cy.get('.list-row').should('contain', 'this is a test todo');
@ -30,21 +29,20 @@ context('Form', () => {
cy.visit('/app/contact/Test Form Contact 3');
cy.get('.prev-doc').should('be.visible').click();
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
cy.get('.btn-modal-close:visible').click();
cy.hide_dialog();
cy.get('.next-doc').click();
cy.wait(200);
cy.hide_dialog();
cy.contains('Test Form Contact 2').should('not.exist');
cy.get('.title-text').should('contain', 'Test Form Contact 1');
cy.get('.title-text').should('contain', 'Test Form Contact 3');
// clear filters
cy.window().its('frappe').then((frappe) => {
let list_view = frappe.get_list_view('Contact');
list_view.filter_area.filter_list.clear_filters();
});
cy.visit('/app/contact');
cy.clear_filters();
});
it('validates behaviour of Data options validations in child table', () => {
// test email validations for set_invalid controller
let website_input = 'website.in';
let expectBackgroundColor = 'rgb(255, 220, 220)';
let expectBackgroundColor = 'rgb(255, 245, 245)';
cy.visit('/app/contact/new');
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');

View file

@ -1,11 +1,11 @@
context('Grid Pagination', () => {
beforeEach(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
});

View file

@ -1,7 +1,7 @@
context('List View', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});
@ -11,20 +11,21 @@ context('List View', () => {
cy.go_to_list('ToDo');
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true });
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
cy.get('.dropdown-menu li:visible').should('have.length', 8).each((el, index) => {
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => {
cy.wrap(el).contains(actions[index]);
}).then((elements) => {
cy.server();
cy.route({
cy.intercept({
method: 'POST',
url: 'api/method/frappe.model.workflow.bulk_workflow_approval'
}).as('bulk-approval');
cy.route({
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.reportview.get'
}).as('real-time-update');
cy.wrap(elements).contains('Approve').click();
cy.wait(['@bulk-approval', '@real-time-update']);
cy.hide_dialog();
cy.clear_filters();
cy.get('.list-row-container:visible').should('contain', 'Approved');
});
});

View file

@ -1,7 +1,7 @@
context('List View Settings', () => {
beforeEach(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
it('Default settings', () => {
cy.visit('/app/List/DocType/List');
@ -14,8 +14,8 @@ context('List View Settings', () => {
cy.wait(300);
cy.get('.list-count').should('contain', "20 of");
cy.get('.menu-btn-group button').click();
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
cy.get('.modal-dialog').should('contain', 'Settings');
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
cy.get('.modal-dialog').should('contain', 'DocType Settings');
cy.get('input[data-fieldname="disable_count"]').check({ force: true });
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true });
@ -27,8 +27,8 @@ context('List View Settings', () => {
cy.get('.list-sidebar .list-tags').should('not.exist');
cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
cy.get('.modal-dialog').should('contain', 'Settings');
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
cy.get('.modal-dialog').should('contain', 'DocType Settings');
cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true });
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true });
cy.get('button').filter(':visible').contains('Save').click();

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', () => {
@ -35,7 +35,7 @@ context('Login', () => {
cy.get('#login_password').type(Cypress.config('adminPassword'));
cy.get('.btn-login:visible').click();
cy.location('pathname').should('eq', '/app/space/Home');
cy.location('pathname').should('eq', '/app');
cy.window().its('frappe.session.user').should('eq', 'Administrator');
});

View file

@ -1,7 +1,7 @@
context('Query Report', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
});
it('add custom column in report', () => {

View file

@ -4,73 +4,69 @@ context('Recorder', () => {
});
it('Navigate to Recorder', () => {
cy.visit('/app/space/Website');
cy.visit('/app');
cy.awesomebar('recorder');
cy.get('h1').should('contain', 'Recorder');
cy.location('pathname').should('eq', '#recorder');
cy.get('h3').should('contain', 'Recorder');
cy.url().should('include', '/recorder/detail');
});
// it('Recorder Empty State', () => {
// cy.visit('/app/recorder');
// cy.get('.title-text').should('contain', 'Recorder');
it('Recorder Empty State', () => {
cy.visit('/app/recorder');
cy.get('.title-text').should('contain', 'Recorder');
// cy.get('.indicator').should('contain', 'Inactive').should('have.class', 'red');
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
// cy.get('.primary-action').should('contain', 'Start');
// cy.get('.btn-secondary').should('contain', 'Clear');
cy.get('.primary-action').should('contain', 'Start');
cy.get('.btn-secondary').should('contain', 'Clear');
// cy.get('.msg-box').should('contain', 'Inactive');
// cy.get('.msg-box .btn-primary').should('contain', 'Start Recording');
// });
cy.get('.msg-box').should('contain', 'Inactive');
cy.get('.msg-box .btn-primary').should('contain', 'Start Recording');
});
// it('Recorder Start', () => {
// cy.visit('/app/recorder');
// cy.get('.primary-action').should('contain', 'Start').click();
// cy.get('.indicator').should('contain', 'Active').should('have.class', 'green');
it('Recorder Start', () => {
cy.visit('/app/recorder');
cy.get('.primary-action').should('contain', 'Start').click();
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
// cy.get('.msg-box').should('contain', 'No Requests');
cy.get('.msg-box').should('contain', 'No Requests');
// cy.server();
// cy.visit('/app/List/DocType/List');
// cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
// cy.wait('@list_refresh');
cy.visit('/app/List/DocType/List');
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.wait('@list_refresh');
// cy.get('.title-text').should('contain', 'DocType');
// cy.get('.list-count').should('contain', '20 of ');
cy.get('.title-text').should('contain', 'DocType');
cy.get('.list-count').should('contain', '20 of ');
// cy.visit('/app/recorder');
// cy.get('.title-text').should('contain', 'Recorder');
// cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
cy.visit('/app/recorder');
cy.get('.title-text').should('contain', 'Recorder');
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
// cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
// cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
// cy.get('.msg-box').should('contain', 'Inactive');
// });
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
cy.wait(500);
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
cy.get('.msg-box').should('contain', 'Inactive');
});
// it('Recorder View Request', () => {
// cy.visit('/app/recorder');
// cy.get('.primary-action').should('contain', 'Start').click();
it('Recorder View Request', () => {
cy.visit('/app/recorder');
cy.get('.primary-action').should('contain', 'Start').click();
// cy.server();
// cy.visit('/app/List/DocType/List');
// cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
// cy.wait('@list_refresh');
cy.visit('/app/List/DocType/List');
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.wait('@list_refresh');
// cy.get('.title-text').should('contain', 'DocType');
// cy.get('.list-count').should('contain', '20 of ');
cy.get('.title-text').should('contain', 'DocType');
cy.get('.list-count').should('contain', '20 of ');
// temporarily commenting out theses tests as they seem to be
// randomly failing maybe due a backround event
cy.visit('/app/recorder');
// cy.visit('/app/recorder');
cy.get('.list-row-container span').contains('/api/method/frappe').click();
// cy.get('.list-row-container span').contains('/api/method/frappe').click();
cy.url().should('include', '/recorder/request');
cy.get('form').should('contain', '/api/method/frappe');
// cy.location('hash').should('contain', '#recorder/request/');
// cy.get('form').should('contain', '/api/method/frappe');
// cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
// cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
// cy.location('hash').should('eq', '#recorder');
// });
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
cy.wait(200);
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
});
});

View file

@ -4,50 +4,43 @@ context('Relative Timeframe', () => {
});
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
});
});
it('sets relative timespan filter for last week and filters list', () => {
cy.visit('/app/List/ToDo/List');
cy.clear_filters();
cy.get('.list-row:contains("this is fourth todo")').should('exist');
cy.add_filter();
// cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
cy.get('select.condition.form-control').select("Timespan");
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
cy.server();
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
// cy.get('.filter-box .btn:contains("Apply")').click();
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-popover .apply-filters').click({ force: true });
cy.wait('@list_refresh');
cy.get('.list-row-container').its('length').should('eq', 1);
cy.get('.list-row-container').should('contain', 'this is second todo');
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
.as('save_user_settings');
cy.clear_filters();
cy.wait('@save_user_settings');
});
it('sets relative timespan filter for next week and filters list', () => {
cy.visit('/app/List/ToDo/List');
cy.clear_filters();
cy.get('.list-row:contains("this is fourth todo")').should('exist');
// cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.add_filter();
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
cy.get('select.condition.form-control').select("Timespan");
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
cy.server();
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
// cy.get('.filter-box .btn:contains("Apply")').click();
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-popover .apply-filters').click({ force: true });
cy.wait('@list_refresh');
// cy.get('.list-row-container').its('length').should('eq', 1);
// cy.get('.list-row').should('contain', 'this is first todo');
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
.as('save_user_settings');
// cy.get('.remove-filter').click();
cy.clear_filters();
cy.wait('@save_user_settings');
});

View file

@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name;
context('Report View', () => {
before(() => {
cy.login();
cy.visit('/app/space/Website');
cy.visit('/app/website');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
cy.insert_doc(doctype_name, {
@ -16,8 +16,7 @@ context('Report View', () => {
}, true).as('doc');
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.server();
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');

View file

@ -17,11 +17,10 @@ context('Table MultiSelect', () => {
.as('selected-value');
cy.get('@selected-value').should('contain', 'test@erpnext.com');
cy.server();
cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
cy.intercept('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
// trigger save
cy.get('.primary-action').click();
cy.wait('@save_form').its('status').should('eq', 200);
cy.wait('@save_form').its('response.statusCode').should('eq', 200);
cy.get('@selected-value').should('contain', 'test@erpnext.com');
});

View file

@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
Cypress.Commands.add('create_records', doc => {
return cy
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
.then(r => r.message);
});
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false, force: true });
cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
@ -204,15 +204,47 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
return cy.get(selector);
});
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
cy.get('@input').click().wait(200);
cy.get('.datepickers-container .datepicker.active').should('exist');
}
if (fieldtype === 'Time') {
cy.get('@input').clear().wait(200);
}
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
selector += ` [data-idx="${row_idx}"]`;
selector += ` .form-in-grid`;
if (fieldtype === 'Text Editor') {
selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
} else if (fieldtype === 'Code') {
selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
} else {
selector += ` .form-control[data-fieldname="${fieldname}"]`;
}
return cy.get(selector);
});
Cypress.Commands.add('awesomebar', text => {
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
});
Cypress.Commands.add('new_form', doctype => {
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-')
// // let route = `${dt_in_route}/new-${dt_in_route}-1`;
// let route = `${dt_in_route}/new`;
// let route = `${doctype.toLowerCase().replace(' ', '-')}/new`;
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
cy.visit(`/app/${dt_in_route}/new`);
cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`);
cy.get('body').should('have.attr', 'data-ajax-state', 'complete');
@ -244,8 +276,7 @@ Cypress.Commands.add('get_open_dialog', () => {
Cypress.Commands.add('hide_dialog', () => {
cy.wait(200);
cy.get_open_dialog()
.find('.btn-modal-close').click()
cy.get_open_dialog().find('.btn-modal-close').click();
cy.get('.modal:visible').should('not.exist');
});
@ -289,4 +320,8 @@ Cypress.Commands.add('clear_filters', () => {
cy.wait(300);
cy.get('.filter-popover').should('exist');
cy.get('.filter-popover').find('.clear-filters').click();
cy.get('.filter-section .filter-button').click();
cy.window().its('cur_list').then(cur_list => {
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
});
});

View file

@ -21,5 +21,5 @@ import './commands';
// require('./commands')
Cypress.Cookies.defaults({
whitelist: 'sid'
preserve: 'sid'
});

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
@ -27,6 +33,7 @@ __version__ = '13.0.0-dev'
__title__ = "Frappe Framework"
local = Local()
controllers = {}
class _dict(dict):
"""dict like object that exposes keys as attributes"""
@ -327,7 +334,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
"""
from frappe.utils import encode
from frappe.utils import strip_html_tags
msg = safe_decode(msg)
out = _dict(message=msg)
@ -354,7 +361,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
out.as_list = 1
if flags.print_messages and out.message:
print(f"Message: {repr(out.message).encode('utf-8')}")
print(f"Message: {strip_html_tags(out.message)}")
if title:
out.title = title
@ -465,7 +472,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
attachments=None, content=None, doctype=None, name=None, reply_to=None,
cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False,
inline_images=None, template=None, args=None, header=None, print_letterhead=False):
inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False):
"""Send email using user's default **Email Account** or global default **Email Account**.
@ -491,6 +498,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
:param template: Name of html template from templates/emails folder
:param args: Arguments for rendering the template
:param header: Append header in email
:param with_container: Wraps email inside a styled container
"""
text_content = None
if template:
@ -513,7 +521,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority,
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
inline_images=inline_images, header=header, print_letterhead=print_letterhead)
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
whitelisted = []
guest_methods = []
@ -628,6 +636,21 @@ def clear_cache(user=None, doctype=None):
local.role_permissions = {}
def only_has_select_perm(doctype, user=None, ignore_permissions=False):
if ignore_permissions:
return False
if not user:
user = local.session.user
import frappe.permissions
permissions = frappe.permissions.get_role_permissions(doctype, user=user)
if permissions.get('select') and not permissions.get('read'):
return True
else:
return False
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
"""Raises `frappe.PermissionError` if not permitted.
@ -948,10 +971,6 @@ def get_installed_apps(sort=False, frappe_last=False):
if not local.all_apps:
local.all_apps = cache().get_value('all_apps', get_all_apps)
#cache bench apps
if not cache().get_value('all_apps'):
cache().set_value('all_apps', local.all_apps)
installed = json.loads(db.get_global("installed_apps") or "[]")
if sort:

View file

@ -7,8 +7,8 @@ import os
from six import iteritems
import logging
from werkzeug.wrappers import Request
from werkzeug.local import LocalManager
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware
@ -57,19 +57,22 @@ def application(request):
frappe.monitor.start()
frappe.rate_limiter.apply()
if frappe.local.form_dict.cmd:
if request.method == "OPTIONS":
response = Response()
elif frappe.form_dict.cmd:
response = frappe.handler.handle()
elif frappe.request.path.startswith("/api/"):
elif request.path.startswith("/api/"):
response = frappe.api.handle()
elif frappe.request.path.startswith('/backups'):
elif request.path.startswith('/backups'):
response = frappe.utils.response.download_backup(request.path)
elif frappe.request.path.startswith('/private/files/'):
elif request.path.startswith('/private/files/'):
response = frappe.utils.response.download_private_file(request.path)
elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()
else:
@ -88,13 +91,9 @@ def application(request):
rollback = after_request(rollback)
finally:
if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
if request.method in ("POST", "PUT") and frappe.db and rollback:
frappe.db.rollback()
# set cookies
if response and hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
@ -110,9 +109,7 @@ def application(request):
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if response and hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
process_response(response)
frappe.destroy()
return response
@ -134,7 +131,46 @@ def init_request(request):
make_form_dict(request)
frappe.local.http_request = frappe.auth.HTTPRequest()
if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest()
def process_response(response):
if not response:
return
# set cookies
if hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
# rate limiter headers
if hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
# CORS headers
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
set_cors_headers(response)
def set_cors_headers(response):
origin = frappe.request.headers.get('Origin')
if not origin:
return
allow_cors = frappe.conf.allow_cors
if allow_cors != "*":
if not isinstance(allow_cors, list):
allow_cors = [allow_cors]
if origin not in allow_cors:
return
response.headers.extend({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
'Cache-Control,Content-Type')
})
def make_form_dict(request):
import json

View file

@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', {
toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
if (frm.doc.reference_doctype) {
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
}
},
template: function(frm) {
@ -100,10 +102,7 @@ frappe.ui.form.on('Auto Repeat', {
frappe.auto_repeat.render_schedule = function(frm) {
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
frm.call("get_auto_repeat_schedule").then(r => {
frm.dashboard.wrapper.empty();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {

View file

@ -23,6 +23,8 @@
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
"section_break_16",
"repeat_on_days",
"notification",
"notify_by_email",
"recipients",
@ -189,15 +191,27 @@
"fieldtype": "Check",
"label": "Repeat on Last Day of the Month"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "repeat_on_days",
"fieldtype": "Table",
"label": "Repeat on Days",
"options": "Auto Repeat Day"
},
{
"default": "0",
"fieldname": "submit_on_creation",
"fieldtype": "Check",
"label": "Submit on Creation"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "section_break_16",
"fieldtype": "Section Break"
}
],
"links": [],
"modified": "2020-12-10 10:43:13.449172",
"modified": "2021-01-12 09:24:49.719611",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
@ -13,9 +14,12 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
class AutoRepeat(Document):
def validate(self):
@ -24,6 +28,7 @@ class AutoRepeat(Document):
self.validate_submit_on_creation()
self.validate_dates()
self.validate_email_id()
self.validate_auto_repeat_days()
self.set_dates()
self.update_auto_repeat_id()
self.unlink_if_applicable()
@ -49,7 +54,7 @@ class AutoRepeat(Document):
if self.disabled:
self.next_schedule_date = None
else:
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
@ -88,6 +93,12 @@ class AutoRepeat(Document):
else:
frappe.throw(_("'Recipients' not specified"))
def validate_auto_repeat_days(self):
auto_repeat_days = self.get_auto_repeat_days()
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
repeated_days = get_repeated(auto_repeat_days)
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
def update_auto_repeat_id(self):
#check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
@ -113,7 +124,7 @@ class AutoRepeat(Document):
end_date = getdate(self.end_date)
if not self.end_date:
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
next_date = self.get_next_schedule_date(schedule_date=start_date)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
@ -122,8 +133,7 @@ class AutoRepeat(Document):
schedule_details.append(row)
if self.end_date:
next_date = get_next_schedule_date(
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
while (getdate(next_date) < getdate(end_date)):
row = {
@ -132,8 +142,7 @@ class AutoRepeat(Document):
"next_scheduled_date" : next_date
}
schedule_details.append(row)
next_date = get_next_schedule_date(
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
return schedule_details
@ -211,6 +220,75 @@ class AutoRepeat(Document):
new_doc.set('from_date', from_date)
new_doc.set('to_date', to_date)
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
"""
Returns the next schedule date for auto repeat after a recurring document has been created.
Adds required offset to the schedule_date param and returns the next schedule date.
:param schedule_date: The date when the last recurring document was created.
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
"""
if month_map.get(self.frequency):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and self.repeat_on_last_day:
day_count = 31
next_date = get_next_date(self.start_date, month_count, day_count)
elif month_count and self.repeat_on_day:
day_count = self.repeat_on_day
next_date = get_next_date(self.start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(self.start_date, month_count)
else:
days = self.get_days(schedule_date)
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(self.frequency, 0)
next_date = get_next_date(self.start_date, month_count, day_count)
else:
days = self.get_days(next_date)
next_date = add_days(next_date, days)
return next_date
def get_days(self, schedule_date):
if self.frequency == "Weekly":
days = self.get_offset_for_weekly_frequency(schedule_date)
else:
# daily frequency
days = 1
return days
def get_offset_for_weekly_frequency(self, schedule_date):
# if weekdays are not set, offset is 7 from current schedule date
if not self.repeat_on_days:
return 7
repeat_on_days = self.get_auto_repeat_days()
current_schedule_day = getdate(schedule_date).weekday()
weekdays = list(week_map.keys())
# if repeats on more than 1 day or
# start date's weekday is not in repeat days, then get next weekday
# else offset is 7
if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
weekday = get_next_weekday(current_schedule_day, repeat_on_days)
next_weekday_number = week_map.get(weekday, 0)
# offset for upcoming weekday
return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
return 7
def get_auto_repeat_days(self):
return [d.day for d in self.get('repeat_on_days', [])]
def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation"""
subject = self.subject or ''
@ -252,13 +330,8 @@ class AutoRepeat(Document):
def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document:
res = frappe.db.get_all('Contact',
fields=['email_id'],
filters=[
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
['Dynamic Link', 'link_name', '=', self.reference_document]
])
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
email_ids = list(set([d.email_id for d in res]))
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
@ -291,42 +364,24 @@ class AutoRepeat(Document):
)
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
if month_map.get(frequency):
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and repeat_on_last_day:
day_count = 31
next_date = get_next_date(start_date, month_count, day_count)
elif month_count and repeat_on_day:
day_count = repeat_on_day
next_date = get_next_date(start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(frequency)
next_date = get_next_date(start_date, month_count, day_count)
elif days:
next_date = add_days(next_date, days)
return next_date
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
def get_next_weekday(current_schedule_day, weekdays):
days = list(week_map.keys())
if current_schedule_day > 0:
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
else:
days = days[(current_schedule_day + 1):]
for entry in days:
if entry in weekdays:
return entry
#called through hooks
def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
@ -337,6 +392,7 @@ def make_auto_repeat_entry():
data = get_auto_repeat_entries(date)
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
@ -346,10 +402,11 @@ def create_repeated_entries(data):
if schedule_date == current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
def get_auto_repeat_entries(date=None):
if not date:
date = getdate(today())
@ -358,6 +415,7 @@ def get_auto_repeat_entries(date=None):
['status', '=', 'Active']
])
#called through hooks
def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
@ -367,6 +425,7 @@ def set_auto_repeat_as_completed():
doc.status = 'Completed'
doc.save()
@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
if not start_date:

View file

@ -7,10 +7,9 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields():
df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase):
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.get_doc('ToDo', new_todo)
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat_with_weekdays(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
weekdays = list(week_map.keys())
current_weekday = getdate().weekday()
days = [
{'day': weekdays[current_weekday]},
{'day': weekdays[(current_weekday + 2) % 7]}
]
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
doc.reload()
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2))
def test_monthly_auto_repeat(self):
start_date = today()
end_date = add_months(start_date, 12)
@ -144,7 +189,8 @@ def make_auto_repeat(**args):
'notify_by_email': args.notify or 0,
'recipients': args.recipients or "",
'subject': args.subject or "",
'message': args.message or ""
'message': args.message or "",
'repeat_on_days': args.days or []
}).insert(ignore_permissions=True)
return doc

View file

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2020-11-10 22:30:53.690228",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"day"
],
"fields": [
{
"fieldname": "day",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Day",
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-10 22:30:53.690228",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat Day",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class DeskShortcut(Document):
class AutoRepeatDay(Document):
pass

View file

@ -109,7 +109,7 @@ def load_conf_settings(bootinfo):
def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_desk_sidebar_items
bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False)
bootinfo.allowed_workspaces = get_desk_sidebar_items()
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard")
@ -250,13 +250,12 @@ def add_home_page(bootinfo, docs):
try:
page = frappe.desk.desk_page.get(home_page)
docs.append(page)
bootinfo['home_page'] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError):
if frappe.message_log:
frappe.message_log.pop()
page = frappe.desk.desk_page.get('space')
bootinfo['home_page'] = page.name
docs.append(page)
bootinfo['home_page'] = 'Workspaces'
def add_timezone_info(bootinfo):
system = bootinfo.sysdefaults.get("time_zone")

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',
@ -68,6 +68,7 @@ def clear_defaults_cache(user=None):
frappe.cache().delete_key("defaults")
def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
cache = frappe.cache()
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
@ -99,6 +100,15 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys:
cache.delete_value(name)
def clear_controller_cache(doctype=None):
if not doctype:
del frappe.controllers
frappe.controllers = {}
return
for site_controllers in frappe.controllers.values():
site_controllers.pop(doctype, None)
def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'

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'

View file

@ -256,3 +256,27 @@ def get_contact_with_phone_number(number):
def get_contact_name(email_id):
contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
return contact[0].parent if contact else None
def get_contacts_linking_to(doctype, docname, fields=None):
"""Return a list of contacts containing a link to the given document."""
return frappe.get_list('Contact', fields=fields, filters=[
['Dynamic Link', 'link_doctype', '=', doctype],
['Dynamic Link', 'link_name', '=', docname]
])
def get_contacts_linked_from(doctype, docname, fields=None):
"""Return a list of contacts that are contained in (linked from) the given document."""
link_fields = frappe.get_meta(doctype).get('fields', {
'fieldtype': 'Link',
'options': 'Contact'
})
if not link_fields:
return []
contact_names = frappe.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields])
if not contact_names:
return []
return frappe.get_list('Contact', fields=fields, filters={
'name': ('in', contact_names)
})

View file

@ -27,6 +27,8 @@ class Comment(Document):
def on_update(self):
update_comment_in_doc(self)
if self.is_new():
self.notify_change('update')
def on_trash(self):
self.remove_comment_from_cache()
@ -163,7 +165,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
try:
# use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
(json.dumps(_comments[-50:]), reference_name))
(json.dumps(_comments[-100:]), reference_name))
except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):

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({message:__('Document not Relinked'), 'indicator': 'info'})
function() {
frappe.show_alert({
message: __('Document not Relinked'), 'indicator': 'info'
});
}
);
}

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"autoname": "hash",
"creation": "2017-01-11 04:21:35.217943",
@ -13,6 +14,7 @@
"column_break_2",
"permlevel",
"section_break_4",
"select",
"read",
"write",
"create",
@ -211,9 +213,16 @@
"fieldtype": "Data",
"label": "Reference Document Type",
"read_only": 1
},
{
"default": "0",
"fieldname": "select",
"fieldtype": "Check",
"label": "Select"
}
],
"modified": "2019-10-31 16:58:16.157079",
"links": [],
"modified": "2020-12-03 15:20:48.296730",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",

View file

@ -751,7 +751,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
"message": _("{0} is a mandatory field asdadsf").format(id_field.label),
"message": _("{0} is a mandatory field").format(id_field.label),
}
)
return

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='/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);

View file

@ -1,775 +1,229 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:27:33",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"role_and_level",
"role",
"if_owner",
"column_break_2",
"permlevel",
"section_break_4",
"select",
"read",
"write",
"create",
"delete",
"column_break_8",
"submit",
"cancel",
"amend",
"additional_permissions",
"report",
"export",
"import",
"set_user_permissions",
"column_break_19",
"share",
"print",
"email"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "role_and_level",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Role and Level",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Role and Level"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "role",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Role",
"length": 0,
"no_copy": 0,
"oldfieldname": "role",
"oldfieldtype": "Link",
"options": "Role",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "150px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "Apply this rule if the User is the Owner",
"fieldname": "if_owner",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "If user is the owner",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "If user is the owner"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Level",
"length": 0,
"no_copy": 0,
"oldfieldname": "permlevel",
"oldfieldtype": "Int",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "40px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "40px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Permissions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Permissions"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "read",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Read",
"length": 0,
"no_copy": 0,
"oldfieldname": "read",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "write",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Write",
"length": 0,
"no_copy": 0,
"oldfieldname": "write",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "create",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Create",
"length": 0,
"no_copy": 0,
"oldfieldname": "create",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "delete",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Delete",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Delete"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "submit",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Submit",
"length": 0,
"no_copy": 0,
"oldfieldname": "submit",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "cancel",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Cancel",
"length": 0,
"no_copy": 0,
"oldfieldname": "cancel",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "amend",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amend",
"length": 0,
"no_copy": 0,
"oldfieldname": "amend",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "additional_permissions",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Additional Permissions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Additional Permissions"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "report",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Report",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "export",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Export",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Export"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "import",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Import",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Import"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "This role update User Permissions for a user",
"fieldname": "set_user_permissions",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Set User Permissions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Set User Permissions"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_19",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "share",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Share",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Share"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "print",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Print",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Print"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "email",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Email",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Email"
},
{
"default": "0",
"fieldname": "select",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Select"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-05-29 11:54:38.613936",
"links": [],
"modified": "2020-12-03 15:15:30.488212",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,
"track_seen": 0
"sort_field": "modified",
"sort_order": "ASC"
}

View file

@ -5,7 +5,7 @@
from __future__ import unicode_literals
import re, copy, os, shutil
import json
from frappe.cache_manager import clear_user_cache
from frappe.cache_manager import clear_user_cache, clear_controller_cache
# imports - third party imports
import six
@ -26,6 +26,7 @@ from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
from frappe.desk.utils import validate_route_conflict
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@ -288,9 +289,15 @@ class DocType(Document):
self.update_fields_to_fetch()
from frappe import conf
allow_doctype_export = frappe.flags.allow_doctype_export or (not frappe.flags.in_test and conf.get('developer_mode'))
if not self.custom and not frappe.flags.in_import and allow_doctype_export:
allow_doctype_export = (
not self.custom
and not frappe.flags.in_import
and (
frappe.conf.developer_mode
or frappe.flags.allow_doctype_export
)
)
if allow_doctype_export:
self.export_doc()
self.make_controller_template()
@ -368,13 +375,10 @@ class DocType(Document):
if merge:
frappe.throw(_("DocType can not be merged"))
# Do not rename and move files and folders for custom doctype
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
def after_rename(self, old, new, merge=False):
"""Change table name using `RENAME TABLE` if table exists. Or update
`doctype` property for Single type."""
if self.issingle:
frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old))
frappe.db.sql("""update tabSingles set value=%s
@ -384,6 +388,18 @@ class DocType(Document):
"mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`",
"postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`"
})
frappe.db.commit()
# Do not rename and move files and folders for custom doctype
if not self.custom:
if not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
clear_controller_cache(old)
def after_delete(self):
if not self.custom:
clear_controller_cache(self.name)
def rename_files_and_folders(self, old, new):
# move files
@ -640,7 +656,7 @@ class DocType(Document):
flags = {"flags": re.ASCII} if six.PY3 else {}
# a DocType name should not start or end with an empty space
if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
@ -648,6 +664,8 @@ class DocType(Document):
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
validate_route_conflict(self.doctype, self.name)
def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""
if frappe.flags.in_patch: return
@ -984,10 +1002,10 @@ def validate_fields(meta):
check_sort_field(meta)
check_image_field(meta)
def validate_permissions_for_doctype(doctype, for_remove=False):
def validate_permissions_for_doctype(doctype, for_remove=False, alert=False):
"""Validates if permissions are set correctly."""
doctype = frappe.get_doc("DocType", doctype)
validate_permissions(doctype, for_remove)
validate_permissions(doctype, for_remove, alert=alert)
# save permissions
for perm in doctype.get("permissions"):
@ -1010,9 +1028,10 @@ def clear_permissions_cache(doctype):
""", doctype):
frappe.clear_cache(user=user)
def validate_permissions(doctype, for_remove=False):
def validate_permissions(doctype, for_remove=False, alert=False):
permissions = doctype.get("permissions")
if not permissions:
# Some DocTypes may not have permissions by default, don't show alert for them
if not permissions and alert:
frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange')
issingle = issubmittable = isimportable = False
if doctype:
@ -1024,7 +1043,7 @@ def validate_permissions(doctype, for_remove=False):
return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx)
def check_atleast_one_set(d):
if not d.read and not d.write and not d.submit and not d.cancel and not d.create:
if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create:
frappe.throw(_("{0}: No basic permissions set").format(get_txt(d)))
def check_double(d):

View file

@ -1,7 +1,7 @@
import frappe
from frappe.desk.utils import get_doctype_route
from frappe.desk.utils import slug
def execute():
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)):
if not doctype.route:
frappe.db.set_value('DocType', doctype.name, 'route', get_doctype_route(doctype.name), update_modified = False)
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False)

View file

@ -5,14 +5,13 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
IllegalMandatoryError,
DoctypeLinkError,
WrongOptionsDoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
CannotIndexedError,
InvalidFieldNameError,
CannotCreateStandardDoctypeError,
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
IllegalMandatoryError,
DoctypeLinkError,
WrongOptionsDoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
CannotIndexedError,
InvalidFieldNameError,
validate_links_table_fieldnames)
# test_records = frappe.get_test_records('DocType')

View file

@ -6,8 +6,19 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
from frappe import _
class DocumentNamingRule(Document):
def validate(self):
self.validate_fields_in_conditions()
def validate_fields_in_conditions(self):
if self.has_value_changed("document_type"):
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
for condition in self.conditions:
if condition.field not in docfields:
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))
def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.

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

View file

@ -43,7 +43,7 @@ class ModuleDef(Document):
def on_trash(self):
"""Delete module name from modules.txt"""
if frappe.flags.in_uninstall or self.custom:
if not frappe.conf.get('developer_mode') or frappe.flags.in_uninstall or self.custom:
return
modules = None

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

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
}

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

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

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,10 +34,7 @@ class Page(Document):
self.name += '-' + str(cnt)
def validate(self):
if frappe.db.get_value('DocType', self.name):
frappe.throw(
_("{} is the name of a DocType. DocType names cannot be the same as a Page name, please choose another name.").format(self.page_name)
)
validate_route_conflict(self.doctype, self.name)
if self.is_new() and not getattr(conf,'developer_mode', 0):
frappe.throw(_("Not in Developer Mode"))

View file

@ -0,0 +1,5 @@
import frappe
def execute():
for name in ('desktop', 'space'):
frappe.delete_doc('Page', name)

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)

View file

@ -44,7 +44,7 @@
},
{
"fieldname": "options",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Options"
},
{
@ -58,7 +58,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 16:15:46.937267",
"modified": "2020-12-05 19:20:00.503097",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Filter",

View file

@ -2,6 +2,7 @@ import frappe
from ..role import desk_properties
def execute():
frappe.reload_doctype('role')
for role in frappe.get_all('Role', ['name', 'desk_access']):
role_doc = frappe.get_doc('Role', role.name)
for key in desk_properties:

View file

@ -31,6 +31,9 @@ class Role(Document):
def set_desk_properties(self):
# set if desk_access is not allowed, unset all desk properties
if self.name == 'Guest':
self.desk_access = 0
if not self.desk_access:
for key in desk_properties:
self.set(key, 0)

View file

@ -47,7 +47,7 @@
"fieldname": "doctype_event",
"fieldtype": "Select",
"label": "DocType Event",
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
},
{
"depends_on": "eval:doc.script_type==='API'",
@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-03 22:42:02.708148",
"modified": "2021-01-03 18:50:14.767595",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -6,6 +6,7 @@ import frappe
EVENT_MAP = {
'before_insert': 'Before Insert',
'after_insert': 'After Insert',
'before_validate': 'Before Validate',
'validate': 'Before Save',
'on_update': 'After Save',
'before_submit': 'Before Submit',

View file

@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase):
def tearDownClass(cls):
frappe.db.commit()
frappe.db.sql('truncate `tabServer Script`')
frappe.cache().delete_value('server_script_map')
def setUp(self):
frappe.cache().delete_value('server_script_map')

View file

@ -358,7 +358,7 @@
"collapsible": 1,
"fieldname": "email",
"fieldtype": "Section Break",
"label": "EMail"
"label": "Email"
},
{
"description": "Your organization name and address for the email footer.",
@ -486,7 +486,7 @@
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2020-11-30 18:52:22.161391",
"modified": "2020-12-30 18:52:22.161391",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@ -504,4 +504,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View file

@ -37,6 +37,25 @@ frappe.ui.form.on('User', {
}
},
module_profile: function(frm) {
if (frm.doc.module_profile) {
frappe.call({
"method": "frappe.core.doctype.user.user.get_module_profile",
args: {
module_profile: frm.doc.module_profile
},
callback: function(data) {
frm.set_value("block_modules", []);
$.each(data.message || [], function(i, v) {
let d = frm.add_child("block_modules");
d.module = v.module;
});
frm.module_editor && frm.module_editor.refresh();
}
});
}
},
onload: function(frm) {
frm.can_edit_roles = has_access_to_edit_user();
@ -255,43 +274,3 @@ function get_roles_for_editing_user() {
.filter(perm => perm.permlevel >= 1 && perm.write)
.map(perm => perm.role) || ['System Manager'];
}
frappe.ModuleEditor = Class.extend({
init: function(frm, wrapper) {
this.wrapper = $('<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-4"><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});
}
});
}
});

View file

@ -55,6 +55,7 @@
"allowed_in_mentions",
"user_emails",
"sb_allow_modules",
"module_profile",
"modules_html",
"block_modules",
"home_settings",
@ -301,7 +302,7 @@
"no_copy": 1
},
{
"default": "0",
"default": "1",
"fieldname": "logout_all_sessions",
"fieldtype": "Check",
"label": "Logout From All Devices After Changing Password"
@ -594,6 +595,12 @@
"fieldtype": "Select",
"label": "Desk Theme",
"options": "Light\nDark"
},
{
"fieldname": "module_profile",
"fieldtype": "Link",
"label": "Module Profile",
"options": "Module Profile"
}
],
"icon": "fa fa-user",
@ -654,10 +661,15 @@
"group": "Activity",
"link_doctype": "ToDo",
"link_fieldname": "owner"
},
{
"group": "Integrations",
"link_doctype": "Token Cache",
"link_fieldname": "user"
}
],
"max_attachments": 5,
"modified": "2020-12-24 19:48:49.677800",
"modified": "2021-02-01 16:11:06.037543",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

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...":
@ -89,6 +90,15 @@ class User(Document):
self.set('roles', [])
self.append_roles(*[role.role for role in role_profile.roles])
def validate_allowed_modules(self):
if self.module_profile:
module_profile = frappe.get_doc('Module Profile', self.module_profile)
self.set('block_modules', [])
for d in module_profile.get('block_modules'):
self.append('block_modules', {
'module': d.module
})
def validate_user_image(self):
if self.user_image and len(self.user_image) > 2000:
frappe.throw(_("Not a valid User Image."))
@ -98,16 +108,17 @@ class User(Document):
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
now=frappe.flags.in_test or frappe.flags.in_install
self.send_password_notification(self.__new_password)
frappe.enqueue(
'frappe.core.doctype.user.user.create_contact',
user=self,
ignore_mandatory=True,
now=frappe.flags.in_test or frappe.flags.in_install
now=now
)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now)
# Set user selected timezone
if self.time_zone:
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
@ -288,16 +299,16 @@ class User(Document):
from frappe.utils.user import get_user_fullname
from frappe.utils import get_url
full_name = get_user_fullname(frappe.session['user'])
if full_name == "Guest":
full_name = "Administrator"
created_by = get_user_fullname(frappe.session['user'])
if created_by == "Guest":
created_by = "Administrator"
args = {
'first_name': self.first_name or self.last_name or "user",
'user': self.name,
'title': subject,
'login_url': get_url(),
'user_fullname': full_name
'created_by': created_by
}
args.update(add_args)
@ -551,6 +562,10 @@ def get_perm_info(role):
@frappe.whitelist(allow_guest=True)
def update_password(new_password, logout_all_sessions=0, key=None, old_password=None):
#validate key to avoid key input like ['like', '%'], '', ['in', ['']]
if key and not isinstance(key, str):
frappe.throw(_('Invalid key type'))
result = test_password_strength(new_password, key, old_password)
feedback = result.get("feedback", None)
@ -1002,9 +1017,14 @@ def send_token_via_email(tmp_id,token=None):
hotp = pyotp.HOTP(otpsecret)
frappe.sendmail(
recipients=user_email, sender=None, subject='Verification Code',
message='<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
@ -1038,6 +1058,11 @@ def get_role_profile(role_profile):
roles = frappe.get_doc('Role Profile', {'role_profile': role_profile})
return roles.roles
@frappe.whitelist()
def get_module_profile(module_profile):
module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile})
return module_profile.get('block_modules')
def update_roles(role_profile):
users = frappe.get_all('User', filters={'role_profile_name': role_profile})
role_profile = frappe.get_doc('Role Profile', role_profile)

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

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

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
}

View file

@ -49,7 +49,8 @@ class UserPermission(Document):
'name': ['!=', self.name]
}, or_filters={
'applicable_for': cstr(self.applicable_for),
'apply_to_all_doctypes': 1
'apply_to_all_doctypes': 1,
'hide_descendants': cstr(self.hide_descendants)
}, limit=1)
if overlap_exists:
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
@ -91,13 +92,13 @@ def get_user_permissions(user=None):
try:
for perm in frappe.get_all('User Permission',
fields=['allow', 'for_value', 'applicable_for', 'is_default'],
fields=['allow', 'for_value', 'applicable_for', 'is_default', 'hide_descendants'],
filters=dict(user=user)):
meta = frappe.get_meta(perm.allow)
add_doc_to_perm(perm, perm.for_value, perm.is_default)
if meta.is_nested_set():
if meta.is_nested_set() and not perm.hide_descendants:
decendants = frappe.db.get_descendants(perm.allow, perm.for_value)
for doc in decendants:
add_doc_to_perm(perm, doc, False)
@ -172,8 +173,8 @@ def check_applicable_doc_perm(user, doctype, docname):
"allow": doctype,
"for_value":docname,
})
for d in data:
applicable.append(d.applicable_for)
for permission in data:
applicable.append(permission.applicable_for)
return applicable
@ -194,7 +195,8 @@ def add_user_permissions(data):
data = json.loads(data)
data = frappe._dict(data)
d = check_applicable_doc_perm(data.user, data.doctype, data.docname)
# get all doctypes on whom this permission is applied
perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname)
exists = frappe.db.exists("User Permission", {
"user": data.user,
"allow": data.doctype,
@ -202,26 +204,27 @@ def add_user_permissions(data):
"apply_to_all_doctypes": 1
})
if data.apply_to_all_doctypes == 1 and not exists:
remove_applicable(d, data.user, data.doctype, data.docname)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1)
remove_applicable(perm_applied_docs, data.user, data.doctype, data.docname)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, apply_to_all=1)
return 1
elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1:
remove_apply_to_all(data.user, data.doctype, data.docname)
update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname)
update_applicable(perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname)
for applicable in data.applicable_doctypes :
if applicable not in d:
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable)
if applicable not in perm_applied_docs:
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable)
elif exists:
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable)
return 1
return 0
def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, applicable=None):
def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, apply_to_all=None, applicable=None):
user_perm = frappe.new_doc("User Permission")
user_perm.user = user
user_perm.allow = doctype
user_perm.for_value = docname
user_perm.is_default = is_default
user_perm.hide_descendants = hide_descendants
if applicable:
user_perm.applicable_for = applicable
user_perm.apply_to_all_doctypes = 0
@ -229,8 +232,8 @@ def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, ap
user_perm.apply_to_all_doctypes = 1
user_perm.insert()
def remove_applicable(d, user, doctype, docname):
for applicable_for in d:
def remove_applicable(perm_applied_docs, user, doctype, docname):
for applicable_for in perm_applied_docs:
frappe.db.sql("""DELETE FROM `tabUser Permission`
WHERE `user`=%s
AND `applicable_for`=%s

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

View file

@ -21,7 +21,7 @@
<td class="danger">{{ item[1] }}</td>
<td class="success">{{ item[2] }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
@ -58,7 +58,7 @@
</table>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
@ -93,4 +93,4 @@
{% endfor %}
</tbody>
{% endif %}
</div>
</div>

View file

@ -1,5 +1,5 @@
frappe.pages["background_jobs"].on_page_load = (wrapper) => {
background_job = new BackgroundJobs(wrapper);
const background_job = new BackgroundJobs(wrapper);
$(wrapper).bind('show', () => {
background_job.show();
@ -20,10 +20,12 @@ class BackgroundJobs {
this.show_failed = false;
this.show_failed_button = this.page.add_inner_button(__("Show Failed Jobs"), () => {
this.show_failed = !this.show_failed
this.show_failed_button && this.show_failed_button.text(
this.show_failed ? __("Hide Failed Jobs") : __("Show Failed Jobs")
)
this.show_failed = !this.show_failed;
if (this.show_failed_button) {
this.show_failed_button.text(
this.show_failed ? __("Hide Failed Jobs") : __("Show Failed Jobs")
);
}
});
$(frappe.render_template('background_jobs_outer')).appendTo(this.page.body);

View file

@ -183,7 +183,7 @@ class Dashboard {
frappe.db.get_list('Dashboard').then(dashboards => {
dashboards.map(dashboard => {
let name = dashboard.name;
if(name != this.dashboard_name){
if (name != this.dashboard_name) {
this.page.add_menu_item(name, () => frappe.set_route("dashboard-view", name), 1);
}
});

View file

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

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

View file

@ -98,7 +98,7 @@ frappe.PermissionEngine = class PermissionEngine {
}
reset_std_permissions(data) {
let doctype = this.get_doctype()
let doctype = this.get_doctype();
let d = frappe.confirm(__("Reset Permissions for {0}?", [doctype]), () => {
return frappe.call({
module: "frappe.core",
@ -117,7 +117,7 @@ frappe.PermissionEngine = class PermissionEngine {
let rights = this.rights
.filter((r) => d[r])
.map((r) => {
return __(toTitle(frappe.unscrub(r)))
return __(toTitle(frappe.unscrub(r)));
});
d.rights = rights.join(", ");
@ -153,16 +153,14 @@ frappe.PermissionEngine = class PermissionEngine {
this.page.clear_primary_action();
if (!this.doctype_select) {
this.set_empty_message(__("Loading"))
return
return this.set_empty_message(__("Loading"));
}
let doctype = this.get_doctype();
let role = this.get_role();
if (!doctype && !role) {
this.set_empty_message(__("Select Document Type or Role to start."))
return;
return this.set_empty_message(__("Select Document Type or Role to start."));
}
// get permissions
@ -202,7 +200,7 @@ frappe.PermissionEngine = class PermissionEngine {
[__("Level"), 40],
[__("Permissions"), 350],
["", 40]
]
];
table_columns.forEach((col) => {
$("<th>")
@ -292,8 +290,8 @@ frappe.PermissionEngine = class PermissionEngine {
}
get rights() {
return ["read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share"]
return ["select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share"];
}
set_show_users(cell, role) {
@ -436,7 +434,7 @@ frappe.PermissionEngine = class PermissionEngine {
d.show();
},
"small-add"
)
);
}
make_reset_button() {
@ -459,4 +457,4 @@ frappe.PermissionEngine = class PermissionEngine {
return frappe.get_children("DocType", doctype, "fields",
{ fieldtype: "Link", options: ["not in", ["User", '[Select]']] });
}
}
};

View file

@ -77,6 +77,18 @@ def add(parent, role, permlevel):
@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None):
"""Update role permission params
Args:
doctype (str): Name of the DocType to update params for
role (str): Role to be updated for, eg "Website Manager".
permlevel (int): perm level the provided rule applies to
ptype (str): permission type, example "read", "delete", etc.
value (None, optional): value for ptype, None indicates False
Returns:
str: Refresh flag is permission is updated successfully
"""
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
return 'refresh' if out else None
@ -92,7 +104,7 @@ def remove(doctype, role, permlevel):
if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
validate_permissions_for_doctype(doctype, for_remove=True)
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
@frappe.whitelist()
def reset(doctype):

View file

@ -2,7 +2,8 @@ frappe.pages['recorder'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: 'Recorder',
single_column: true
single_column: true,
card_layout: true
});
frappe.recorder = new Recorder(wrapper);

View file

@ -1,12 +0,0 @@
frappe.pages['space'].on_page_load = function (wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
name: 'space',
title: __("Workspace"),
});
frappe.workspace = new frappe.views.Workspace(wrapper);
$(wrapper).bind('show', function () {
frappe.workspace.show();
});
}

View file

@ -1,23 +0,0 @@
{
"content": null,
"creation": "2020-02-27 15:07:57.124916",
"docstatus": 0,
"doctype": "Page",
"icon": "icon-th",
"idx": 0,
"modified": "2020-12-16 14:22:05.591912",
"modified_by": "Administrator",
"module": "Core",
"name": "space",
"owner": "Administrator",
"page_name": "space",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0
}

View file

@ -0,0 +1,211 @@
{
"cards_label": "Elements",
"category": "Modules",
"charts": [],
"creation": "2021-01-02 10:51:16.579957",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends_another_page": 0,
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"is_standard": 1,
"label": "Build",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Modules",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Module Def",
"link_to": "Module Def",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Module Onboarding",
"link_to": "Module Onboarding",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Block Module",
"link_to": "Block Module",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Models",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Views",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Report",
"link_to": "Report",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Scripting",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Server Script",
"link_to": "Server Script",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Script",
"link_to": "Custom Script",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Scheduled Job Type",
"link_to": "Scheduled Job Type",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
}
],
"modified": "2021-01-02 14:03:15.029699",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"doc_view": "",
"label": "DocType",
"link_to": "DocType",
"type": "DocType"
},
{
"doc_view": "",
"label": "Workspace",
"link_to": "Workspace",
"type": "DocType"
},
{
"doc_view": "",
"label": "Report",
"link_to": "Report",
"type": "DocType"
}
]
}

View file

@ -33,6 +33,7 @@ frappe.ui.form.on('Custom Script', {
}
],
primary_action: ({ cdt }) => {
cdt = d.get_field('cdt').value;
frm.events.add_script_for_doctype(frm, cdt);
d.hide();
}

View file

@ -4,16 +4,35 @@
frappe.provide("frappe.customize_form");
frappe.ui.form.on("Customize Form", {
setup: function(frm) {
// save the last setting if refreshing
window.addEventListener("beforeunload", () => {
if (frm.doc.doc_type && frm.doc.doc_type != "undefined") {
localStorage["customize_doctype"] = frm.doc.doc_type;
}
});
},
onload: function(frm) {
frm.disable_save();
frm.set_query("doc_type", function() {
return {
translate_values: false,
filters: [
['DocType', 'issingle', '=', 0],
['DocType', 'custom', '=', 0],
['DocType', 'name', 'not in', frappe.model.core_doctypes_list],
['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains]
["DocType", "issingle", "=", 0],
["DocType", "custom", "=", 0],
[
"DocType",
"name",
"not in",
frappe.model.core_doctypes_list
],
[
"DocType",
"restrict_to_domain",
"in",
frappe.boot.active_domains
]
]
};
});
@ -21,15 +40,15 @@ frappe.ui.form.on("Customize Form", {
frm.set_query("default_print_format", function() {
return {
filters: {
'print_format_type': ['!=', 'JS'],
'doc_type': ['=', frm.doc.doc_type]
print_format_type: ["!=", "JS"],
doc_type: ["=", frm.doc.doc_type]
}
}
};
});
$(frm.wrapper).on("grid-row-render", function(e, grid_row) {
if (grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
$(grid_row.row).css({"font-weight": "bold"});
if (grid_row.doc && grid_row.doc.fieldtype == "Section Break") {
$(grid_row.row).css({ "font-weight": "bold" });
}
});
@ -40,12 +59,6 @@ frappe.ui.form.on("Customize Form", {
$(frm.wrapper).on("grid-move-row", function(e, frm) {
frm.trigger("setup_sortable");
});
if (localStorage['customize_doctype']) {
// set default value from customize form
frm.set_value('doc_type', localStorage['customize_doctype']);
}
},
doc_type: function(frm) {
@ -59,7 +72,6 @@ frappe.ui.form.on("Customize Form", {
if (r._server_messages && r._server_messages.length) {
frm.set_value("doc_type", "");
} else {
localStorage['customize_doctype'] = frm.doc.doc_type;
frm.refresh();
frm.trigger("setup_sortable");
}
@ -72,9 +84,11 @@ frappe.ui.form.on("Customize Form", {
},
setup_sortable: function(frm) {
frm.page.body.find('.highlight').removeClass('highlight');
frm.page.body.find(".highlight").removeClass("highlight");
frm.doc.fields.forEach(function(f, i) {
var data_row = frm.page.body.find('[data-fieldname="fields"] [data-idx="'+ f.idx +'"] .data-row');
var data_row = frm.page.body.find(
'[data-fieldname="fields"] [data-idx="' + f.idx + '"] .data-row'
);
if (f.is_custom_field) {
data_row.addClass("highlight");
@ -82,9 +96,13 @@ frappe.ui.form.on("Customize Form", {
f._sortable = false;
}
if (f.fieldtype == "Table") {
frm.add_custom_button(f.options, function() {
frm.set_value('doc_type', f.options);
}, __('Customize Child Table'));
frm.add_custom_button(
f.options,
function() {
frm.set_value("doc_type", f.options);
},
__("Customize Child Table")
);
}
});
frm.fields_dict.fields.grid.refresh();
@ -97,36 +115,91 @@ frappe.ui.form.on("Customize Form", {
if (frm.doc.doc_type) {
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() {
frappe.set_route('List', frm.doc.doc_type);
}, __('Actions'));
frm.add_custom_button(
__("Go to {0} List", [frm.doc.doc_type]),
function() {
frappe.set_route("List", frm.doc.doc_type);
},
__("Actions")
);
frm.add_custom_button(__('Reload'), function() {
frm.script_manager.trigger("doc_type");
}, __('Actions'));
frm.add_custom_button(
__("Reload"),
function() {
frm.script_manager.trigger("doc_type");
},
__("Actions")
);
frm.add_custom_button(__('Reset to defaults'), function() {
frappe.customize_form.confirm(__('Remove all customizations?'), frm);
}, __('Actions'));
frm.add_custom_button(
__("Reset to defaults"),
function() {
frappe.customize_form.confirm(
__("Remove all customizations?"),
frm
);
},
__("Actions")
);
frm.add_custom_button(__('Set Permissions'), function() {
frappe.set_route('permission-manager', frm.doc.doc_type);
}, __('Actions'));
frm.add_custom_button(
__("Set Permissions"),
function() {
frappe.set_route("permission-manager", frm.doc.doc_type);
},
__("Actions")
);
}
if (frappe.boot.developer_mode) {
frm.add_custom_button(__('Export Customizations'), function() {
frm.events.setup_export(frm);
frm.events.setup_sort_order(frm);
frm.events.set_default_doc_type(frm);
},
set_default_doc_type(frm) {
let doc_type;
if (frappe.route_options && frappe.route_options.doc_type) {
doc_type = frappe.route_options.doc_type;
frappe.route_options = null;
localStorage.removeItem("customize_doctype");
}
if (!doc_type) {
doc_type = localStorage.getItem("customize_doctype");
}
if (doc_type) {
setTimeout(() => frm.set_value("doc_type", doc_type), 1000);
}
},
setup_export(frm) {
if (frappe.boot.developer_mode) {
frm.add_custom_button(
__("Export Customizations"),
function() {
frappe.prompt(
[
{fieldtype:'Link', fieldname:'module', options:'Module Def',
label: __('Module to Export')},
{fieldtype:'Check', fieldname:'sync_on_migrate',
label: __('Sync on Migrate'), 'default': 1},
{fieldtype:'Check', fieldname:'with_permissions',
label: __('Export Custom Permissions'), 'default': 1},
{
fieldtype: "Link",
fieldname: "module",
options: "Module Def",
label: __("Module to Export")
},
{
fieldtype: "Check",
fieldname: "sync_on_migrate",
label: __("Sync on Migrate"),
default: 1
},
{
fieldtype: "Check",
fieldname: "with_permissions",
label: __("Export Custom Permissions"),
default: 1
}
],
function(data) {
frappe.call({
method: 'frappe.modules.utils.export_customizations',
method: "frappe.modules.utils.export_customizations",
args: {
doctype: frm.doc.doc_type,
module: data.module,
@ -135,27 +208,25 @@ frappe.ui.form.on("Customize Form", {
}
});
},
__("Select Module"));
}, __('Actions'));
}
__("Select Module")
);
},
__("Actions")
);
}
},
setup_sort_order(frm) {
// sort order select
if (frm.doc.doc_type) {
var fields = $.map(frm.doc.fields,
function(df) {
return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null;
});
var fields = $.map(frm.doc.fields, function(df) {
return frappe.model.is_value_type(df.fieldtype)
? df.fieldname
: null;
});
fields = ["", "name", "modified"].concat(fields);
frm.set_df_property("sort_field", "options", fields);
}
if (frappe.route_options && frappe.route_options.doc_type) {
setTimeout(function() {
frm.set_value("doc_type", frappe.route_options.doc_type);
frappe.route_options = null;
}, 1000);
}
}
});

View file

@ -455,11 +455,15 @@ class CustomizeForm(Document):
self.fetch_to_customize()
def reset_customization(doctype):
frappe.db.sql("""
DELETE FROM `tabProperty Setter` WHERE doc_type=%s
and `field_name`!='naming_series'
and `property`!='options'
""", doctype)
setters = frappe.get_all("Property Setter", filters={
'doc_type': doctype,
'field_name': ['!=', 'naming_series'],
'property': ['!=', 'options']
}, pluck='name')
for setter in setters:
frappe.delete_doc("Property Setter", setter)
frappe.clear_cache(doctype=doctype)
doctype_properties = {

View file

@ -4,12 +4,11 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.utils import get_doctype_route
from frappe.desk.utils import slug
class DocTypeLayout(Document):
def validate(self):
if not self.route:
self.route = get_doctype_route(self.name)
self.route = slug(self.name)

View file

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Test rename new', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,42 @@
{
"actions": [],
"creation": "2021-01-13 12:47:03.572640",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"random"
],
"fields": [
{
"fieldname": "random",
"fieldtype": "Data",
"label": "random"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-13 12:47:03.572640",
"modified_by": "Administrator",
"module": "Custom",
"name": "Test rename new",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"route": "test-rename",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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