Merge branch 'develop' into newsletter-modifications

This commit is contained in:
Jannat Patel 2021-02-22 14:38:30 +05:30 committed by GitHub
commit 5e30d54bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
779 changed files with 32637 additions and 41668 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,
@ -146,6 +147,7 @@
"context": true,
"before": true,
"beforeEach": true,
"qz": true
"qz": true,
"localforage": 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/

View file

@ -14,7 +14,6 @@ pull_request_rules:
- name: Automatic squash on CI success and review
conditions:
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk (frappe)
- label!=dont-merge

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

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

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,12 +2,12 @@ context('API Resources', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
it('Creates two Comments', () => {
cy.insert_doc('Comment', {comment_type: 'Comment', content: "hello"});
cy.insert_doc('Comment', {comment_type: 'Comment', content: "world"});
cy.insert_doc('Comment', { comment_type: 'Comment', content: "hello" });
cy.insert_doc('Comment', { comment_type: 'Comment', content: "world" });
});
it('Lists the Comments', () => {

View file

@ -2,11 +2,11 @@ context('Awesome Bar', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
beforeEach(() => {
cy.get('.navbar-header .navbar-home').click();
cy.get('.navbar .navbar-home').click();
});
it('navigates to doctype list', () => {
@ -14,16 +14,16 @@ context('Awesome Bar', () => {
cy.get('#navbar-search + ul').should('be.visible');
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
cy.get('h1').should('contain', 'To Do');
cy.get('.title-text').should('contain', 'To Do');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.location('pathname').should('eq', '/app/todo');
});
it('find text in doctype list', () => {
cy.get('#navbar-search')
.type('test in todo{downarrow}{enter}', { delay: 200 });
cy.get('h1').should('contain', 'To Do');
cy.get('.title-text').should('contain', 'To Do');
cy.get('[data-original-title="Name"] > .input-with-feedback')
.should('have.value', '%test%');
@ -33,7 +33,7 @@ context('Awesome Bar', () => {
cy.get('#navbar-search')
.type('new blog post{downarrow}{enter}', { delay: 200 });
cy.get('.title-text:visible').should('have.text', 'New Blog Post 1');
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
});
it('calculates math expressions', () => {

View file

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

View file

@ -1,10 +1,10 @@
context('Control Duration', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
function get_dialog_with_duration(hide_days=0, hide_seconds=0) {
function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) {
return cy.dialog({
title: 'Duration',
fields: [{
@ -22,11 +22,11 @@ context('Control Duration', () => {
.first()
.click();
cy.get('.duration-input[data-duration=days]')
.type(45, {force: true})
.blur({force: true});
.type(45, { force: true })
.blur({ force: true });
cy.get('.duration-input[data-duration=minutes]')
.type(30)
.blur({force: true});
.blur({ force: true });
cy.get('.frappe-control[data-fieldname=duration] input').first().should('have.value', '45d 30m');
cy.get('.frappe-control[data-fieldname=duration] input').first().blur();
cy.get('.duration-picker').should('not.be.visible');

View file

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

View file

@ -1,7 +1,7 @@
context('Control Rating', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
function get_dialog_with_rating() {
@ -18,7 +18,7 @@ context('Control Rating', () => {
get_dialog_with_rating().as('dialog');
cy.get('div.rating')
.children('i.fa')
.children('svg')
.first()
.click()
.should('have.class', 'star-click');
@ -33,11 +33,11 @@ context('Control Rating', () => {
get_dialog_with_rating();
cy.get('div.rating')
.children('i.fa')
.children('svg')
.first()
.invoke('trigger', 'mouseenter')
.should('have.class', 'star-hover')
.invoke('trigger', 'mouseleave')
.should('not.have.class', 'star-hover');
});
});
});

View file

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

View file

@ -1,7 +1,7 @@
context('Depends On', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
name: 'Child Test Depends On',
@ -64,7 +64,7 @@ context('Depends On', () => {
cy.fill_field('test_field', 'Some Value');
cy.get('button.primary-action').contains('Save').click();
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
cy.get('body').click();
cy.hide_dialog();
cy.fill_field('test_field', 'Random value');
cy.get('button.primary-action').contains('Save').click();
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
@ -92,7 +92,7 @@ context('Depends On', () => {
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click();
cy.get('@row1-form_in_grid').find('.grid-collapse-row').click();
// set the table to read-only
cy.fill_field('test_field', 'Some Other Value');

View file

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

View file

@ -1,56 +1,50 @@
context('Form', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.visit('/app/todo/new');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
cy.wait(300);
cy.get('.page-title').should('contain', 'Not Saved');
cy.server();
cy.route({
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.form.save.savedocs'
}).as('form_save');
cy.get('.primary-action').click();
cy.wait('@form_save').its('status').should('eq', 200);
cy.visit('/desk#List/ToDo');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('h1').should('be.visible').and('contain', 'To Do');
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
cy.visit('/app/todo');
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
cy.get('.list-row').should('contain', 'this is a test todo');
});
it('navigates between documents with child table list filters applied', () => {
cy.visit('/desk#List/Contact');
cy.location('hash').should('eq', '#List/Contact/List');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type('Number{enter}', { force: true });
cy.visit('/app/contact');
cy.add_filter();
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
cy.get('.filter-box .btn:contains("Apply")').click({ force: true });
cy.visit('/desk#Form/Contact/Test Form Contact 3');
cy.get('.filter-popover .apply-filters').click({ force: true });
cy.visit('/app/contact/Test Form Contact 3');
cy.get('.prev-doc').should('be.visible').click();
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
cy.get('.btn-modal-close:visible').click();
cy.hide_dialog();
cy.get('.next-doc').click();
cy.wait(200);
cy.hide_dialog();
cy.contains('Test Form Contact 2').should('not.exist');
cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1');
cy.get('.title-text').should('contain', 'Test Form Contact 3');
// clear filters
cy.window().its('frappe').then((frappe) => {
let list_view = frappe.get_list_view('Contact');
list_view.filter_area.filter_list.clear_filters();
});
cy.visit('/app/contact');
cy.clear_filters();
});
it('validates behaviour of Data options validations in child table', () => {
// test email validations for set_invalid controller
let website_input = 'website.in';
let expectBackgroundColor = 'rgb(255, 220, 220)';
let expectBackgroundColor = 'rgb(255, 245, 245)';
cy.visit('/desk#Form/Contact/New Contact 1');
cy.visit('/app/contact/new');
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();

View file

@ -1,24 +1,24 @@
context('Grid Pagination', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
});
});
it('creates pages for child table', () => {
cy.visit('/desk#Form/Contact/Test Contact');
cy.visit('/app/contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.current-page-number').should('contain', '1');
cy.get('@table').find('.total-page-number').should('contain', '20');
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
});
it('goes to the next and previous page', () => {
cy.visit('/desk#Form/Contact/Test Contact');
cy.visit('/app/contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.next-page').click();
cy.get('@table').find('.current-page-number').should('contain', '2');
@ -27,21 +27,21 @@ context('Grid Pagination', () => {
cy.get('@table').find('.current-page-number').should('contain', '1');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
});
it('adds and deletes rows and changes page', ()=> {
cy.visit('/desk#Form/Contact/Test Contact');
it('adds and deletes rows and changes page', () => {
cy.visit('/app/contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
cy.get('@table').find('.current-page-number').should('contain', '21');
cy.get('@table').find('.total-page-number').should('contain', '21');
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true});
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true });
cy.get('@table').find('button.grid-remove-rows').click();
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
cy.get('@table').find('.current-page-number').should('contain', '20');
cy.get('@table').find('.total-page-number').should('contain', '20');
});
// it('deletes all rows', ()=> {
// cy.visit('/desk#Form/Contact/Test Contact');
// cy.visit('/app/contact/Test Contact');
// cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
// cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
// cy.get('@table').find('button.grid-remove-all-rows').click();

View file

@ -1,30 +1,31 @@
context('List View', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});
});
it('enables "Actions" button', () => {
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete'];
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
cy.go_to_list('ToDo');
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true });
cy.get('.btn.btn-primary.btn-sm.dropdown-toggle').contains('Actions').should('be.visible').click();
cy.get('.dropdown-menu li:visible').should('have.length', 7).each((el, index) => {
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => {
cy.wrap(el).contains(actions[index]);
}).then((elements) => {
cy.server();
cy.route({
cy.intercept({
method: 'POST',
url:'api/method/frappe.model.workflow.bulk_workflow_approval'
url: 'api/method/frappe.model.workflow.bulk_workflow_approval'
}).as('bulk-approval');
cy.route({
cy.intercept({
method: 'POST',
url:'api/method/frappe.desk.reportview.get'
url: 'api/method/frappe.desk.reportview.get'
}).as('real-time-update');
cy.wrap(elements).contains('Approve').click();
cy.wait(['@bulk-approval', '@real-time-update']);
cy.hide_dialog();
cy.clear_filters();
cy.get('.list-row-container:visible').should('contain', 'Approved');
});
});

View file

@ -1,36 +1,36 @@
context('List View Settings', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
it('Default settings', () => {
cy.visit('/desk#List/DocType/List');
cy.visit('/app/List/DocType/List');
cy.get('.list-count').should('contain', "20 of");
cy.get('.sidebar-stat').should('contain', "Tags");
cy.get('.list-stats').should('contain', "Tags");
});
it('disable count and sidebar stats then verify', () => {
cy.wait(300);
cy.visit('/desk#List/DocType/List');
cy.visit('/app/List/DocType/List');
cy.wait(300);
cy.get('.list-count').should('contain', "20 of");
cy.get('button').contains('Menu').click();
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
cy.get('.modal-dialog').should('contain', 'Settings');
cy.get('.menu-btn-group button').click();
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
cy.get('.modal-dialog').should('contain', 'DocType Settings');
cy.get('input[data-fieldname="disable_count"]').check({force: true});
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true});
cy.get('input[data-fieldname="disable_count"]').check({ force: true });
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true });
cy.get('button').filter(':visible').contains('Save').click();
cy.reload();
cy.reload({ force: true });
cy.get('.list-count').should('be.empty');
cy.get('.list-sidebar .sidebar-stat').should('not.exist');
cy.get('.list-sidebar .list-tags').should('not.exist');
cy.get('button').contains('Menu').click({force: true});
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
cy.get('.modal-dialog').should('contain', 'Settings');
cy.get('input[data-fieldname="disable_count"]').uncheck({force: true});
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({force: true});
cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
cy.get('.modal-dialog').should('contain', 'DocType Settings');
cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true });
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true });
cy.get('button').filter(':visible').contains('Save').click();
});
});

View file

@ -2,7 +2,7 @@ context('Login', () => {
beforeEach(() => {
cy.request('/api/method/logout');
cy.visit('/login');
cy.location().should('be', '/login');
cy.location('pathname').should('eq', '/login');
});
it('greets with login screen', () => {
@ -11,13 +11,13 @@ context('Login', () => {
it('validates password', () => {
cy.get('#login_email').type('Administrator');
cy.get('.btn-login').click();
cy.get('.btn-login:visible').click();
cy.location('pathname').should('eq', '/login');
});
it('validates email', () => {
cy.get('#login_password').type('qwe');
cy.get('.btn-login').click();
cy.get('.btn-login:visible').click();
cy.location('pathname').should('eq', '/login');
});
@ -25,8 +25,8 @@ context('Login', () => {
cy.get('#login_email').type('Administrator');
cy.get('#login_password').type('qwer');
cy.get('.btn-login').click();
cy.get('.page-card-head').contains('Invalid Login. Try again.');
cy.get('.btn-login:visible').click();
cy.get('.btn-login:visible').contains('Invalid Login. Try again.');
cy.location('pathname').should('eq', '/login');
});
@ -34,8 +34,8 @@ context('Login', () => {
cy.get('#login_email').type('Administrator');
cy.get('#login_password').type(Cypress.config('adminPassword'));
cy.get('.btn-login').click();
cy.location('pathname').should('eq', '/desk');
cy.get('.btn-login:visible').click();
cy.location('pathname').should('eq', '/app');
cy.window().its('frappe.session.user').should('eq', 'Administrator');
});
@ -60,7 +60,7 @@ context('Login', () => {
cy.get('#login_email').type('Administrator');
cy.get('#login_password').type(Cypress.config('adminPassword'));
cy.get('.btn-login').click();
cy.get('.btn-login:visible').click();
// verify redirected location and url params after login
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));

View file

@ -1,33 +1,33 @@
context('Query Report', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
});
it('add custom column in report', () => {
cy.visit('/desk#query-report/Permitted Documents For User');
cy.visit('/app/query-report/Permitted Documents For User');
cy.get('div[class="page-form flex"]', {timeout: 60000}).should('have.length', 1).then(()=>{
cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => {
cy.get('#page-query-report input[data-fieldname="user"]').as('input');
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 });
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur();
cy.wait(300);
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test');
cy.get('@input-test').focus().type('Role', { delay: 100 }).blur();
cy.get('.datatable').should('exist');
cy.get('button').contains('Menu').click({force: true});
cy.get('.dropdown-menu li').contains('Add Column').click({force: true});
cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').contains('Add Column').click({ force: true });
cy.get('.modal-dialog').should('contain', 'Add Column');
cy.get('select[data-fieldname="doctype"]').select("Role", {force: true});
cy.get('select[data-fieldname="field"]').select("Role Name", {force: true});
cy.get('select[data-fieldname="insert_after"]').select("Name", {force: true});
cy.get('button').contains('Submit').click({force: true});
cy.get('button').contains('Menu').click({force: true});
cy.get('.dropdown-menu li').contains('Save').click({force: true});
cy.get('select[data-fieldname="doctype"]').select("Role", { force: true });
cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
cy.get('button').contains('Submit').click({ force: true });
cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').contains('Save').click({ force: true });
cy.get('.modal-dialog').should('contain', 'Save Report');
cy.get('input[data-fieldname="report_name"]').type("Test Report", {delay:100, force: true});
cy.get('button').contains('Submit').click({timeout:1000, force: true});
cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true });
cy.get('button').contains('Submit').click({ timeout: 1000, force: true });
});
});
});

View file

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

View file

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

View file

@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name;
context('Report View', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app/website');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
cy.insert_doc(doctype_name, {
@ -16,15 +16,14 @@ context('Report View', () => {
}, true).as('doc');
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.server();
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/desk#List/${doctype_name}/Report`);
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
// select the cell
cell.dblclick();
cell.find('input[data-fieldname="enabled"]').check({force: true});
cell.find('input[data-fieldname="enabled"]').check({ force: true });
cy.get('.dt-row-0 > .dt-cell--col-5').click();
cy.wait('@value-update');
cy.get('@doc').then(doc => {

View file

@ -13,15 +13,14 @@ context('Table MultiSelect', () => {
cy.get('input[data-fieldname="users"]').focus().as('input');
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
cy.get('@input').type('test{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value')
.first().as('selected-value');
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value .btn-link-to-form')
.as('selected-value');
cy.get('@selected-value').should('contain', 'test@erpnext.com');
cy.server();
cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
cy.intercept('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
// trigger save
cy.get('.primary-action').click();
cy.wait('@save_form').its('status').should('eq', 200);
cy.wait('@save_form').its('response.statusCode').should('eq', 200);
cy.get('@selected-value').should('contain', 'test@erpnext.com');
});
@ -46,6 +45,6 @@ context('Table MultiSelect', () => {
cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click();
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value').as('existing_value');
cy.get('@existing_value').find('.btn-link-to-form').click();
cy.location('hash').should('contain', 'Form/User/test@erpnext.com');
cy.location('pathname').should('contain', '/user/test@erpnext.com');
});
});

View file

@ -244,14 +244,14 @@ Cypress.Commands.add('awesomebar', text => {
});
Cypress.Commands.add('new_form', doctype => {
let route = `Form/${doctype}/New ${doctype} 1`;
cy.visit(`/desk#${route}`);
cy.get('body').should('have.attr', 'data-route', route);
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
cy.visit(`/app/${dt_in_route}/new`);
cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`);
cy.get('body').should('have.attr', 'data-ajax-state', 'complete');
});
Cypress.Commands.add('go_to_list', doctype => {
cy.visit(`/desk#List/${doctype}/List`);
cy.visit(`/app/list/${doctype}/list`);
});
Cypress.Commands.add('clear_cache', () => {
@ -275,9 +275,8 @@ Cypress.Commands.add('get_open_dialog', () => {
});
Cypress.Commands.add('hide_dialog', () => {
cy.get_open_dialog()
.find('.btn-modal-close')
.click();
cy.wait(300);
cy.get_open_dialog().find('.btn-modal-close').click();
cy.get('.modal:visible').should('not.exist');
});
@ -307,4 +306,21 @@ Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
return res.body.data;
});
});
});
});
Cypress.Commands.add('add_filter', () => {
cy.get('.filter-section .filter-button').click();
cy.wait(300);
cy.get('.filter-popover').should('exist');
});
Cypress.Commands.add('clear_filters', () => {
cy.get('.filter-section .filter-button').click();
cy.wait(300);
cy.get('.filter-popover').should('exist');
cy.get('.filter-popover').find('.clear-filters').click();
cy.get('.filter-section .filter-button').click();
cy.window().its('cur_list').then(cur_list => {
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
});
});

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
@ -466,7 +472,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
attachments=None, content=None, doctype=None, name=None, reply_to=None,
cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False,
inline_images=None, template=None, args=None, header=None, print_letterhead=False):
inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False):
"""Send email using user's default **Email Account** or global default **Email Account**.
@ -492,6 +498,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
:param template: Name of html template from templates/emails folder
:param args: Arguments for rendering the template
:param header: Append header in email
:param with_container: Wraps email inside a styled container
"""
text_content = None
if template:
@ -514,7 +521,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority,
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
inline_images=inline_images, header=header, print_letterhead=print_letterhead)
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
whitelisted = []
guest_methods = []
@ -964,10 +971,6 @@ def get_installed_apps(sort=False, frappe_last=False):
if not local.all_apps:
local.all_apps = cache().get_value('all_apps', get_all_apps)
#cache bench apps
if not cache().get_value('all_apps'):
cache().set_value('all_apps', local.all_apps)
installed = json.loads(db.get_global("installed_apps") or "[]")
if sort:
@ -1632,7 +1635,7 @@ def log_error(message=None, title=_("Error")):
method=title)).insert(ignore_permissions=True)
def get_desk_link(doctype, name):
html = '<a href="#Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
html = '<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
return html.format(
doctype=doctype,
name=name,

View file

@ -181,6 +181,9 @@ def make_form_dict(request):
else:
args = request.form or request.args
if not isinstance(args, dict):
frappe.throw("Invalid request arguments")
try:
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \
for k, v in iteritems(args) })

View file

@ -173,7 +173,7 @@ class LoginManager:
frappe.local.cookie_manager.set_cookie("system_user", "yes")
if not resume:
frappe.local.response['message'] = 'Logged In'
frappe.local.response["home_page"] = "/desk"
frappe.local.response["home_page"] = "/app"
if not resume:
frappe.response["full_name"] = self.full_name

View file

@ -1,70 +0,0 @@
{
"cards": [
{
"hidden": 0,
"label": "Tools",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
},
{
"hidden": 0,
"label": "Email",
"links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Automation",
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]"
},
{
"hidden": 0,
"label": "Event Streaming",
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 14:53:24.980279",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Tools",
"modified": "2020-07-21 19:32:18.480700",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"label": "ToDo",
"link_to": "ToDo",
"type": "DocType"
},
{
"label": "Note",
"link_to": "Note",
"type": "DocType"
},
{
"label": "File",
"link_to": "File",
"type": "DocType"
},
{
"label": "Assignment Rule",
"link_to": "Assignment Rule",
"type": "DocType"
},
{
"label": "Auto Repeat",
"link_to": "Auto Repeat",
"type": "DocType"
}
]
}

View file

@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
refresh: function(frm) {
// auto repeat message
if (frm.is_new()) {
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
let customize_form_link = `<a href="/app/customize form">${__('Customize Form')}</a>`;
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
}
@ -106,8 +106,9 @@ frappe.auto_repeat.render_schedule = function(frm) {
frm.dashboard.wrapper.empty();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {
schedule_details : r.message || []
})
schedule_details: r.message || []
}),
__('Auto Repeat Schedule')
);
frm.dashboard.show();
});

View file

@ -15,6 +15,8 @@ from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
@ -328,13 +330,8 @@ class AutoRepeat(Document):
def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document:
res = frappe.db.get_all('Contact',
fields=['email_id'],
filters=[
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
['Dynamic Link', 'link_name', '=', self.reference_document]
])
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
email_ids = list(set([d.email_id for d in res]))
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)

View file

@ -0,0 +1,229 @@
{
"category": "Administration",
"charts": [],
"creation": "2020-03-02 14:53:24.980279",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends_another_page": 0,
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"is_standard": 1,
"label": "Tools",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Tools",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "To Do",
"link_to": "ToDo",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Calendar",
"link_to": "Event",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Note",
"link_to": "Note",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Files",
"link_to": "File",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Activity",
"link_to": "activity",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Email Group",
"link_to": "Email Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Automation",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Assignment Rule",
"link_to": "Assignment Rule",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Milestone",
"link_to": "Milestone",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Auto Repeat",
"link_to": "Auto Repeat",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Event Streaming",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Event Producer",
"link_to": "Event Producer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Event Consumer",
"link_to": "Event Consumer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Event Update Log",
"link_to": "Event Update Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Event Sync Log",
"link_to": "Event Sync Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Document Type Mapping",
"link_to": "Document Type Mapping",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2020-12-01 13:38:39.950350",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"label": "ToDo",
"link_to": "ToDo",
"type": "DocType"
},
{
"label": "Note",
"link_to": "Note",
"type": "DocType"
},
{
"label": "File",
"link_to": "File",
"type": "DocType"
},
{
"label": "Assignment Rule",
"link_to": "Assignment Rule",
"type": "DocType"
},
{
"label": "Auto Repeat",
"link_to": "Auto Repeat",
"type": "DocType"
}
]
}

View file

@ -21,7 +21,7 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
def get_bootinfo():
"""build and return boot info"""
@ -39,7 +39,7 @@ def get_bootinfo():
bootinfo.server_date = frappe.utils.nowdate()
if frappe.session['user'] != 'Guest':
bootinfo.user_info = get_fullnames()
bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session['sid']
bootinfo.modules = {}
@ -48,6 +48,7 @@ def get_bootinfo():
bootinfo.letter_heads = get_letter_heads()
bootinfo.active_domains = frappe.get_active_domains()
bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")]
add_layouts(bootinfo)
bootinfo.module_app = frappe.local.module_app
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})]
@ -61,6 +62,7 @@ def get_bootinfo():
doclist.extend(get_meta_bundle("Page"))
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
bootinfo.notification_settings = get_notification_settings()
# ipinfo
if frappe.session.data.get('ipinfo'):
@ -88,6 +90,8 @@ def get_bootinfo():
bootinfo.frequently_visited_links = frequently_visited_links()
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo()
return bootinfo
@ -106,11 +110,9 @@ def load_conf_settings(bootinfo):
if key in conf: bootinfo[key] = conf.get(key)
def load_desktop_data(bootinfo):
from frappe.config import get_modules_from_all_apps_for_user
from frappe.desk.desktop import get_desk_sidebar_items
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False)
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
bootinfo.allowed_workspaces = get_desk_sidebar_items()
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard")
def get_allowed_pages(cache=False):
@ -222,19 +224,18 @@ def load_translations(bootinfo):
bootinfo["__messages"] = messages
def get_fullnames():
"""map of user fullnames"""
ret = frappe.db.sql("""select `name`, full_name as fullname,
user_image as image, gender, email, username, bio, location, interest, banner_image, allowed_in_mentions
from tabUser where enabled=1 and user_type!='Website User'""", as_dict=1)
def get_user_info():
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image',
'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'],
filters=dict(enabled=1))
d = {}
for r in ret:
# if not r.image:
# r.image = get_gravatar(r.name)
d[r.name] = r
user_info_map = {d.name: d for d in user_info}
return d
admin_data = user_info_map.get('Administrator')
if admin_data:
user_info_map[admin_data.email] = admin_data
return user_info_map
def get_user(bootinfo):
"""get user info"""
@ -251,13 +252,12 @@ def add_home_page(bootinfo, docs):
try:
page = frappe.desk.desk_page.get(home_page)
docs.append(page)
bootinfo['home_page'] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError):
if frappe.message_log:
frappe.message_log.pop()
page = frappe.desk.desk_page.get('workspace')
bootinfo['home_page'] = page.name
docs.append(page)
bootinfo['home_page'] = 'Workspaces'
def add_timezone_info(bootinfo):
system = bootinfo.sysdefaults.get("time_zone")
@ -273,7 +273,7 @@ def load_print(bootinfo, doclist):
def load_print_css(bootinfo, print_settings):
import frappe.www.printview
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Modern", for_legacy=True)
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True)
def get_unseen_notes():
return frappe.db.sql('''select `name`, title, content, notify_on_every_login from `tabNote` where notify_on_login=1
@ -308,3 +308,24 @@ def get_additional_filters_from_hooks():
filter_config.update(frappe.get_attr(hook)())
return filter_config
def add_layouts(bootinfo):
# add routes for readable doctypes
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type'])
def get_desk_settings():
role_list = frappe.get_all('Role', fields=['*'], filters=dict(
name=['in', frappe.get_roles()]
))
desk_settings = {}
from frappe.core.doctype.role.role import desk_properties
for role in role_list:
for key in desk_properties:
desk_settings[key] = desk_settings.get(key) or role.get(key)
return desk_settings
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)

View file

@ -13,7 +13,7 @@ common_default_keys = ["__default", "__global"]
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
'milestone_tracker_map', 'event_consumer_document_type_map')
global_cache_keys = ("app_hooks", "installed_apps",
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
@ -67,10 +67,6 @@ def clear_defaults_cache(user=None):
elif frappe.flags.in_install!="frappe":
frappe.cache().delete_key("defaults")
def clear_document_cache():
frappe.local.document_cache = {}
frappe.cache().delete_key("document_cache")
def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
cache = frappe.cache()
@ -78,9 +74,11 @@ def clear_doctype_cache(doctype=None):
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
del frappe.local.meta_cache[doctype]
for key in ('is_table', 'doctype_modules'):
for key in ('is_table', 'doctype_modules', 'document_cache'):
cache.delete_value(key)
frappe.local.document_cache = {}
def clear_single(dt):
for name in doctype_cache_keys:
cache.hdel(name, dt)
@ -102,15 +100,12 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys:
cache.delete_value(name)
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
clear_document_cache()
def clear_controller_cache(doctype=None):
if not doctype:
del frappe.controllers
frappe.controllers = {}
return
for site_controllers in frappe.controllers.values():
site_controllers.pop(doctype, None)

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

@ -108,4 +108,4 @@ def is_domain(module):
return module.get("category") == "Domains"
def is_module(module):
return module.get("type") == "module"
return module.get("type") == "module"

View file

@ -1,59 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
data = [
{
"label": _("Automation"),
"icon": "fa fa-random",
"items": [
{
"type": "doctype",
"name": "Assignment Rule",
"description": _("Set up rules for user assignments.")
},
{
"type": "doctype",
"name": "Milestone",
"description": _("Tracks milestones on the lifecycle of a document if it undergoes multiple stages.")
},
{
"type": "doctype",
"name": "Auto Repeat",
"description": _("Automatically generates recurring documents.")
},
]
},
{
"label": _("Event Streaming"),
"icon": "fa fa-random",
"items": [
{
"type": "doctype",
"name": "Event Producer",
"description": _("The site you want to subscribe to for consuming events.")
},
{
"type": "doctype",
"name": "Event Consumer",
"description": _("The site which is consuming your events.")
},
{
"type": "doctype",
"name": "Event Update Log",
"description": _("Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.")
},
{
"type": "doctype",
"name": "Event Sync Log",
"description": _("Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.")
},
{
"type": "doctype",
"name": "Document Type Mapping",
"description": _("The mapping configuration between two doctypes.")
}
]
}
]
return data

View file

@ -1,66 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
"label": _("Documents"),
"items": [
{
"type": "doctype",
"name": "DocType",
"description": _("Models (building blocks) of the Application"),
},
{
"type": "doctype",
"name": "Module Def",
"description": _("Groups of DocTypes"),
},
{
"type": "doctype",
"name": "Page",
"description": _("Pages in Desk (place holders)"),
},
{
"type": "doctype",
"name": "Report",
"description": _("Script or Query reports"),
},
{
"type": "doctype",
"name": "Print Format",
"description": _("Customized Formats for Printing, Email"),
},
{
"type": "doctype",
"name": "Custom Script",
"description": _("Client side script extensions in Javascript"),
}
]
},
{
"label": _("Logs"),
"items": [
{
"type": "doctype",
"name": "Error Log",
"description": _("Errors in Background Events"),
},
{
"type": "doctype",
"name": "Email Queue",
"description": _("Background Email Queue"),
},
{
"type": "page",
"label": _("Background Jobs"),
"name": "background_jobs",
},
{
"type": "doctype",
"name": "Error Snapshot",
"description": _("A log of request errors"),
},
]
}
]

View file

@ -1,60 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
"label": _("Form Customization"),
"icon": "fa fa-glass",
"items": [
{
"type": "doctype",
"name": "Customize Form",
"description": _("Change field properties (hide, readonly, permission etc.)")
},
{
"type": "doctype",
"name": "Custom Field",
"description": _("Add fields to forms.")
},
{
"type": "doctype",
"name": "Custom Script",
"description": _("Add custom javascript to forms.")
},
{
"type": "doctype",
"name": "DocType",
"description": _("Add custom forms.")
},
]
},
{
"label": _("Dashboards"),
"items": [
{
"type": "doctype",
"name": "Dashboard",
},
{
"type": "doctype",
"name": "Dashboard Chart",
},
{
"type": "doctype",
"name": "Dashboard Chart Source",
},
]
},
{
"label": _("Other"),
"items": [
{
"type": "doctype",
"label": _("Custom Translations"),
"name": "Translation",
"description": _("Add your own translations")
}
]
}
]

View file

@ -1,67 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
"label": _("Tools"),
"icon": "octicon octicon-briefcase",
"items": [
{
"type": "doctype",
"name": "ToDo",
"label": _("To Do"),
"description": _("Documents assigned to you and by you."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Event",
"label": _("Calendar"),
"link": "List/Event/Calendar",
"description": _("Event and other calendars."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Note",
"description": _("Private and public Notes."),
"onboard": 1,
},
{
"type": "doctype",
"name": "File",
"label": _("Files"),
},
{
"type": "page",
"label": _("Chat"),
"name": "chat",
"description": _("Chat messages and other notifications."),
"data_doctype": "Communication"
},
{
"type": "page",
"label": _("Activity"),
"name": "activity",
"description": _("Activity log of all users."),
},
]
},
{
'label': _('Email'),
'items': [
{
"type": "doctype",
"name": "Newsletter",
"description": _("Newsletters to contacts, leads."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Email Group",
"description": _("Email Group List"),
},
]
}
]

View file

@ -1,133 +0,0 @@
from __future__ import unicode_literals
import frappe
from frappe import _
def get_data():
return [
# Administration
{
"module_name": "Desk",
"category": "Administration",
"label": _("Tools"),
"color": "#FFF5A7",
"reverse": 1,
"icon": "octicon octicon-calendar",
"type": "module",
"description": "Todos, notes, calendar and newsletter."
},
{
"module_name": "Settings",
"category": "Administration",
"label": _("Settings"),
"color": "#bdc3c7",
"reverse": 1,
"icon": "octicon octicon-settings",
"type": "module",
"description": "Data import, printing, email and workflows."
},
{
"module_name": "Automation",
"category": "Administration",
"label": _("Automation"),
"color": "#bdc3c7",
"reverse": 1,
"icon": "octicon octicon-gist",
"type": "module",
"description": "Auto Repeat, Assignment Rule, Milestone Tracking and Event Streaming."
},
{
"module_name": "Users and Permissions",
"category": "Administration",
"label": _("Users and Permissions"),
"color": "#bdc3c7",
"reverse": 1,
"icon": "octicon octicon-settings",
"type": "module",
"description": "Setup roles and permissions for users on documents."
},
{
"module_name": "Customization",
"category": "Administration",
"label": _("Customization"),
"color": "#bdc3c7",
"reverse": 1,
"icon": "octicon octicon-settings",
"type": "module",
"description": "Customize forms, custom fields, scripts and translations."
},
{
"module_name": "Integrations",
"category": "Administration",
"label": _("Integrations"),
"color": "#16a085",
"icon": "octicon octicon-globe",
"type": "module",
"description": "DropBox, Woocomerce, AWS, Shopify and GoCardless."
},
{
"module_name": 'Contacts',
"category": "Administration",
"label": _("Contacts"),
"type": 'module',
"icon": "octicon octicon-book",
"color": '#ffaedb',
"description": "People Contacts and Address Book."
},
{
"module_name": "Core",
"category": "Administration",
"_label": _("Developer"),
"label": "Developer",
"color": "#589494",
"icon": "octicon octicon-circuit-board",
"type": "module",
"system_manager": 1,
"condition": getattr(frappe.local.conf, 'developer_mode', 0),
"description": "Doctypes, dev tools and logs."
},
# Places
{
"module_name": "Website",
"category": "Places",
"label": _("Website"),
"_label": _("Website"),
"color": "#16a085",
"icon": "octicon octicon-globe",
"type": "module",
"description": "Webpages, webforms, blogs and website theme."
},
{
"module_name": 'Social',
"category": "Places",
"label": _('Social'),
"icon": "octicon octicon-heart",
"type": 'link',
"link": '#social/home',
"color": '#FF4136',
'standard': 1,
'idx': 15,
"description": "Build your profile and share posts with other users."
},
{
"module_name": 'Leaderboard',
"category": "Places",
"label": _('Leaderboard'),
"icon": "fa fa-trophy",
"type": 'link',
"link": '#leaderboard/User',
"color": '#FF4136',
'standard': 1,
},
{
"module_name": 'dashboard',
"category": "Places",
"label": _('Dashboard'),
"icon": "octicon octicon-graph",
"type": "link",
"link": "#dashboard",
"color": '#FF4136',
'standard': 1,
'idx': 10
},
]

View file

@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
source_link = "https://github.com/frappe/frappe_io"
docs_base_url = "/docs"

View file

@ -1,127 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
"label": _("Payments"),
"icon": "fa fa-star",
"items": [
{
"type": "doctype",
"name": "Braintree Settings",
"description": _("Braintree payment gateway settings"),
},
{
"type": "doctype",
"name": "PayPal Settings",
"description": _("PayPal payment gateway settings"),
},
{
"type": "doctype",
"name": "Razorpay Settings",
"description": _("Razorpay Payment gateway settings"),
},
{
"type": "doctype",
"name": "Stripe Settings",
"description": _("Stripe payment gateway settings"),
},
{
"type": "doctype",
"name": "Paytm Settings",
"description": _("Paytm payment gateway settings"),
},
]
},
{
"label": _("Backup"),
"items": [
{
"type": "doctype",
"name": "Dropbox Settings",
"description": _("Dropbox backup settings"),
},
{
"type": "doctype",
"name": "S3 Backup Settings",
"description": _("S3 Backup Settings"),
},
{
"type": "doctype",
"name": "Google Drive",
"description": _("Google Drive Backup."),
}
]
},
{
"label": _("Authentication"),
"items": [
{
"type": "doctype",
"name": "Social Login Key",
"description": _("Enter keys to enable login via Facebook, Google, GitHub."),
},
{
"type": "doctype",
"name": "LDAP Settings",
"description": _("Ldap settings"),
},
{
"type": "doctype",
"name": "OAuth Client",
"description": _("Register OAuth Client App"),
},
{
"type": "doctype",
"name": "OAuth Provider Settings",
"description": _("Settings for OAuth Provider"),
},
{
"type": "doctype",
"name": "Connected App",
"description": _("Connect to any OAuth Provider"),
},
]
},
{
"label": _("Webhook"),
"items": [
{
"type": "doctype",
"name": "Webhook",
"description": _("Webhooks calling API requests into web apps"),
},
{
"type": "doctype",
"name": "Slack Webhook URL",
"description": _("Slack Webhooks for internal integration"),
},
]
},
{
"label": _("Google Services"),
"items": [
{
"type": "doctype",
"name": "Google Settings",
"description": _("Google API Settings."),
},
{
"type": "doctype",
"name": "Google Contacts",
"description": _("Google Contacts Integration."),
},
{
"type": "doctype",
"name": "Google Calendar",
"description": _("Google Calendar Integration."),
},
{
"type": "doctype",
"name": "Google Drive",
"description": _("Google Drive Integration."),
}
]
}
]

View file

@ -1,195 +0,0 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.moduleview import add_setup_section
def get_data():
data = [
{
"label": _("Core"),
"icon": "fa fa-wrench",
"items": [
{
"type": "doctype",
"name": "System Settings",
"label": _("System Settings"),
"description": _("Language, Date and Time settings"),
"hide_count": True
},
{
"type": "doctype",
"name": "Global Defaults",
"label": _("Global Defaults"),
"description": _("Company, Fiscal Year and Currency defaults"),
"hide_count": True
},
{
"type": "doctype",
"name": "Log Settings",
"description": _("Log cleanup and notification configuration")
},
{
"type": "doctype",
"name": "Error Log",
"description": _("Log of error on automated events (scheduler).")
},
{
"type": "doctype",
"name": "Error Snapshot",
"description": _("Log of error during requests.")
},
{
"type": "doctype",
"name": "Domain Settings",
"label": _("Domain Settings"),
"description": _("Enable / Disable Domains"),
"hide_count": True
},
]
},
{
"label": _("Data"),
"icon": "fa fa-th",
"items": [
{
"type": "doctype",
"name": "Data Import",
"label": _("Import Data"),
"icon": "octicon octicon-cloud-upload",
"description": _("Import Data from CSV / Excel files.")
},
{
"type": "doctype",
"name": "Data Export",
"label": _("Export Data"),
"icon": "octicon octicon-cloud-upload",
"description": _("Export Data in CSV / Excel format.")
},
{
"type": "doctype",
"name": "Naming Series",
"description": _("Set numbering series for transactions."),
"hide_count": True
},
{
"type": "doctype",
"name": "Rename Tool",
"label": _("Bulk Rename"),
"description": _("Rename many items by uploading a .csv file."),
"hide_count": True
},
{
"type": "doctype",
"name": "Bulk Update",
"label": _("Bulk Update"),
"description": _("Update many values at one time."),
"hide_count": True
},
{
"type": "page",
"name": "backups",
"label": _("Download Backups"),
"description": _("List of backups available for download"),
"icon": "fa fa-download"
},
{
"type": "doctype",
"name": "Deleted Document",
"label": _("Deleted Documents"),
"description": _("Restore or permanently delete a document.")
},
]
},
{
"label": _("Email / Notifications"),
"icon": "fa fa-envelope",
"items": [
{
"type": "doctype",
"name": "Email Account",
"description": _("Add / Manage Email Accounts.")
},
{
"type": "doctype",
"name": "Email Domain",
"description": _("Add / Manage Email Domains.")
},
{
"type": "doctype",
"name": "Notification",
"description": _("Setup Notifications based on various criteria.")
},
{
"type": "doctype",
"name": "Email Template",
"description": _("Email Templates for common queries.")
},
{
"type": "doctype",
"name": "Auto Email Report",
"description": _("Setup Reports to be emailed at regular intervals"),
},
{
"type": "doctype",
"name": "Newsletter",
"description": _("Create and manage newsletter")
},
{
"type": "doctype",
"route": "Form/Notification Settings/{}".format(frappe.session.user),
"name": "Notification Settings",
"description": _("Configure notifications for mentions, assignments, energy points and more.")
}
]
},
{
"label": _("Printing"),
"icon": "fa fa-print",
"items": [
{
"type": "page",
"label": _("Print Format Builder"),
"name": "print-format-builder",
"description": _("Drag and Drop tool to build and customize Print Formats.")
},
{
"type": "doctype",
"name": "Print Settings",
"description": _("Set default format, page size, print style etc.")
},
{
"type": "doctype",
"name": "Print Format",
"description": _("Customized HTML Templates for printing transactions.")
},
{
"type": "doctype",
"name": "Print Style",
"description": _("Stylesheets for Print Formats")
},
]
},
{
"label": _("Workflow"),
"icon": "fa fa-random",
"items": [
{
"type": "doctype",
"name": "Workflow",
"description": _("Define workflows for forms.")
},
{
"type": "doctype",
"name": "Workflow State",
"description": _("States for workflow (e.g. Draft, Approved, Cancelled).")
},
{
"type": "doctype",
"name": "Workflow Action",
"description": _("Actions for workflow (e.g. Approve, Cancel).")
},
]
}
]
add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe")
return data

View file

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

View file

@ -1,85 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
"label": _("Users"),
"icon": "fa fa-group",
"items": [
{
"type": "doctype",
"name": "User",
"description": _("System and Website Users")
},
{
"type": "doctype",
"name": "Role",
"description": _("User Roles")
},
{
"type": "doctype",
"name": "Role Profile",
"description": _("Role Profile")
}
]
},
{
"label": _("Permissions"),
"icon": "fa fa-lock",
"items": [
{
"type": "page",
"name": "permission-manager",
"label": _("Role Permissions Manager"),
"icon": "fa fa-lock",
"description": _("Set Permissions on Document Types and Roles")
},
{
"type": "doctype",
"name": "User Permission",
"label": _("User Permissions"),
"icon": "fa fa-lock",
"description": _("Restrict user for specific document")
},
{
"type": "doctype",
"name": "Role Permission for Page and Report",
"description": _("Set custom roles for page and report")
},
{
"type": "report",
"is_query_report": True,
"doctype": "User",
"icon": "fa fa-eye-open",
"name": "Permitted Documents For User",
"description": _("Check which Documents are readable by a User")
},
{
"type": "report",
"doctype": "DocShare",
"icon": "fa fa-share",
"name": "Document Share Report",
"description": _("Report of all document shares")
}
]
},
{
"label": _("Logs"),
"icon": "fa fa-group",
"items": [
{
"type": "doctype",
"name": "Activity Log",
"label": _("Activity Log"),
"description": _("Activity Log by ")
},
{
"type": "doctype",
"name": "Access Log",
"label": _("Access Log"),
"description": _("View Log of all print, download and export events")
}
]
}
]

View file

@ -1,117 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
"label": _("Web Site"),
"icon": "fa fa-star",
"items": [
{
"type": "doctype",
"name": "Web Page",
"description": _("Content web page."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Web Form",
"description": _("User editable form on Website."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Website Sidebar",
},
{
"type": "doctype",
"name": "Website Slideshow",
"description": _("Embed image slideshows in website pages."),
},
{
"type": "doctype",
"name": "Website Route Meta",
"description": _("Add meta tags to your web pages"),
},
]
},
{
"label": _("Blog"),
"items": [
{
"type": "doctype",
"name": "Blog Post",
"description": _("Single Post (article)."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Blogger",
"description": _("A user who posts blogs."),
},
{
"type": "doctype",
"name": "Blog Category",
"description": _("Categorize blog posts."),
},
]
},
{
"label": _("Setup"),
"icon": "fa fa-cog",
"items": [
{
"type": "doctype",
"name": "Website Settings",
"description": _("Setup of top navigation bar, footer and logo."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Website Theme",
"description": _("List of themes for Website."),
"onboard": 1,
},
{
"type": "doctype",
"name": "Website Script",
"description": _("Javascript to append to the head section of the page."),
},
{
"type": "doctype",
"name": "About Us Settings",
"description": _("Settings for About Us Page."),
},
{
"type": "doctype",
"name": "Contact Us Settings",
"description": _("Settings for Contact Us Page."),
},
]
},
{
"label": _("Portal"),
"items": [
{
"type": "doctype",
"name": "Portal Settings",
"label": _("Portal Settings"),
"onboard": 1,
}
]
},
{
"label": _("Knowledge Base"),
"items": [
{
"type": "doctype",
"name": "Help Category",
},
{
"type": "doctype",
"name": "Help Article",
},
]
},
]

View file

@ -34,8 +34,8 @@
"email_ids",
"phone_nos",
"contact_details",
"is_primary_contact",
"links",
"is_primary_contact",
"more_info",
"department",
"unsubscribed"
@ -248,8 +248,9 @@
"icon": "fa fa-user",
"idx": 1,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-04-06 18:25:28.223693",
"modified": "2020-08-27 14:12:09.906719",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Contact",

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

@ -1,74 +0,0 @@
{
"cards": [
{
"hidden": 0,
"label": "Data",
"links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Email / Notifications",
"links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Website",
"links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Core",
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Printing",
"links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Workflow",
"links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]"
}
],
"category": "Modules",
"charts": [],
"creation": "2020-03-02 15:09:40.527211",
"developer_mode_only": 0,
"disable_user_customization": 1,
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Settings",
"modified": "2020-07-14 10:09:09.520557",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
"owner": "Administrator",
"pin_to_bottom": 1,
"pin_to_top": 0,
"shortcuts": [
{
"icon": "octicon octicon-settings",
"label": "System Settings",
"link_to": "System Settings",
"type": "DocType"
},
{
"icon": "fa fa-print",
"label": "Print Settings",
"link_to": "Print Settings",
"type": "DocType"
},
{
"icon": "fa fa-globe",
"label": "Website Settings",
"link_to": "Website Settings",
"type": "DocType"
}
],
"shortcuts_label": "Settings"
}

View file

@ -1,59 +0,0 @@
{
"cards": [
{
"hidden": 0,
"label": "Users",
"links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Logs",
"links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Permissions",
"links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 15:12:16.754449",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"idx": 0,
"is_standard": 1,
"label": "Users",
"modified": "2020-04-26 22:36:14.311554",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"label": "User",
"link_to": "User",
"type": "DocType"
},
{
"label": "Role",
"link_to": "Role",
"type": "DocType"
},
{
"label": "Permission Manager",
"link_to": "permission-manager",
"type": "Page"
},
{
"label": "User Profile",
"link_to": "user-profile",
"type": "Page"
}
]
}

View file

@ -18,10 +18,7 @@ from frappe.exceptions import ImplicitCommitError
class Comment(Document):
def after_insert(self):
self.notify_mentions()
frappe.publish_realtime('new_communication', self.as_dict(),
doctype=self.reference_doctype, docname=self.reference_name,
after_commit=True)
self.notify_change('add')
def validate(self):
if not self.comment_email:
@ -30,12 +27,30 @@ class Comment(Document):
def on_update(self):
update_comment_in_doc(self)
if self.is_new():
self.notify_change('update')
def on_trash(self):
self.remove_comment_from_cache()
frappe.publish_realtime('delete_communication', self.as_dict(),
doctype= self.reference_doctype, docname = self.reference_name,
after_commit=True)
self.notify_change('delete')
def notify_change(self, action):
key_map = {
'Like': 'like_logs',
'Assigned': 'assignment_logs',
'Assignment Completed': 'assignment_logs',
'Comment': 'comments',
'Attachment': 'attachment_logs',
'Attachment Removed': 'attachment_logs',
}
key = key_map.get(self.comment_type)
if not key: return
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
'doc': self.as_dict(),
'key': key,
'action': action
}, after_commit=True)
def remove_comment_from_cache(self):
_comments = get_comments_from_parent(self)

View file

@ -99,8 +99,7 @@ frappe.ui.form.on("Communication", {
}
},
show_relink_dialog: function(frm){
var lib = "frappe.email";
show_relink_dialog: function(frm) {
var d = new frappe.ui.Dialog ({
title: __("Relink Communication"),
fields: [{
@ -138,8 +137,10 @@ frappe.ui.form.on("Communication", {
}
});
},
function () {
frappe.show_alert('Document not Relinked')
function() {
frappe.show_alert({
message: __('Document not Relinked'), 'indicator': 'info'
});
}
);
}

View file

@ -99,10 +99,7 @@ class Communication(Document):
frappe.db.set_value("Communication", self.reference_name, "status", "Replied")
if self.communication_type == "Communication":
# send new comment to listening clients
frappe.publish_realtime('new_communication', self.as_dict(),
doctype=self.reference_doctype, docname=self.reference_name,
after_commit=True)
self.notify_change('add')
elif self.communication_type in ("Chat", "Notification", "Bot"):
if self.reference_name == frappe.session.user:
@ -125,10 +122,14 @@ class Communication(Document):
def on_trash(self):
if self.communication_type == "Communication":
# send delete comment to listening clients
frappe.publish_realtime('delete_communication', self.as_dict(),
doctype= self.reference_doctype, docname = self.reference_name,
after_commit=True)
self.notify_change('delete')
def notify_change(self, action):
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
'doc': self.as_dict(),
'key': 'communications',
'action': action
}, after_commit=True)
def set_status(self):
if not self.is_new():
@ -244,9 +245,7 @@ class Communication(Document):
if delivery_status:
self.db_set('delivery_status', delivery_status)
frappe.publish_realtime('update_communication', self.as_dict(),
doctype=self.reference_doctype, docname=self.reference_name, after_commit=True)
self.notify_change('update')
# for list views and forms
self.notify_update()

View file

@ -32,7 +32,7 @@ frappe.ui.form.on('Data Import Legacy', {
frm.reload_doc();
}
if (data.progress) {
let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar");
let progress_bar = $(frm.dashboard.progress_area.body).find(".progress-bar");
if (progress_bar) {
$(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped");
$(progress_bar).css("width", data.progress + "%");

View file

@ -9,15 +9,16 @@ frappe.listview_settings["Deleted Document"] = {
args: { docnames },
callback: function (r) {
if (r.message) {
function body(docnames) {
let body = (docnames) => {
const html = docnames.map(docname => {
return `<li><a href='/desk#Form/Deleted Document/${docname}'>${docname}</a></li>`;
return `<li><a href='/app/deleted-document/${docname}'>${docname}</a></li>`;
});
return "<br><ul>" + html.join("");
}
function message(title, docnames) {
};
let message = (title, docnames) => {
return (docnames.length > 0) ? title + body(docnames) + "</ul>": "";
}
};
const { restored, invalid, failed } = r.message;
const restored_summary = message(__("Documents restored successfully"), restored);

View file

@ -24,11 +24,11 @@ frappe.ui.form.on('DocType', {
if (!frm.is_new() && !frm.doc.istable) {
if (frm.doc.issingle) {
frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => {
frappe.set_route('Form', frm.doc.name);
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
} else {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
frappe.set_route('List', frm.doc.name, 'List');
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
}
}

View file

@ -132,7 +132,7 @@
"label": "Editable Grid"
},
{
"default": "1",
"default": "0",
"depends_on": "eval:!doc.istable && !doc.issingle",
"description": "Open a dialog with mandatory fields to create a new record quickly",
"fieldname": "quick_entry",
@ -427,7 +427,7 @@
"label": "Allow Guest to View"
},
{
"depends_on": "has_web_view",
"depends_on": "eval:!doc.istable",
"fieldname": "route",
"fieldtype": "Data",
"label": "Route"
@ -555,7 +555,7 @@
},
{
"group": "Customization",
"link_doctype": "Custom Script",
"link_doctype": "Client Script",
"link_fieldname": "dt"
},
{
@ -609,7 +609,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2020-09-24 13:13:58.227153",
"modified": "2021-02-04 15:10:09.227205",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -637,6 +637,7 @@
"write": 1
}
],
"route": "doctype",
"search_fields": "module",
"show_name_in_global_search": 1,
"sort_field": "modified",

View file

@ -26,7 +26,7 @@ from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
from frappe.desk.utils import validate_route_conflict
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@ -63,6 +63,37 @@ class DocType(Document):
self.validate_name()
self.set_defaults_for_single_and_table()
self.scrub_field_names()
self.set_default_in_list_view()
self.set_default_translatable()
self.validate_series()
self.validate_document_type()
validate_fields(self)
if not self.istable:
validate_permissions(self)
self.make_amendable()
self.make_repeatable()
self.validate_nestedset()
self.validate_website()
validate_links_table_fieldnames(self)
if not self.is_new():
self.before_update = frappe.get_doc('DocType', self.name)
self.setup_fields_to_fetch()
check_email_append_to(self)
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
def after_insert(self):
# clear user cache so that on the next reload this doctype is included in boot
clear_user_cache(frappe.session.user)
def set_defaults_for_single_and_table(self):
if self.issingle:
self.allow_import = 0
self.is_submittable = 0
@ -72,44 +103,6 @@ class DocType(Document):
self.allow_import = 0
self.permissions = []
self.scrub_field_names()
self.set_default_in_list_view()
self.set_default_translatable()
self.validate_series()
self.validate_document_type()
validate_fields(self)
if self.istable:
# no permission records for child table
self.permissions = []
else:
validate_permissions(self)
self.make_amendable()
self.make_repeatable()
self.validate_nestedset()
self.validate_website()
self.validate_links_table_fieldnames()
if not self.is_new():
self.before_update = frappe.get_doc('DocType', self.name)
if not self.is_new():
self.setup_fields_to_fetch()
check_email_append_to(self)
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def after_insert(self):
# clear user cache so that on the next reload this doctype is included in boot
clear_user_cache(frappe.session.user)
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@ -134,6 +127,10 @@ class DocType(Document):
if not frappe.conf.get("developer_mode") and not self.custom:
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def setup_fields_to_fetch(self):
'''Setup query to update values for newly set fetch values'''
try:
@ -192,6 +189,9 @@ class DocType(Document):
def validate_website(self):
"""Ensure that website generator has field 'route'"""
if self.route:
self.route = self.route.strip('/')
if self.has_web_view:
# route field must be present
if not 'route' in [d.fieldname for d in self.fields]:
@ -278,7 +278,6 @@ class DocType(Document):
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
self.delete_duplicate_custom_fields()
try:
frappe.db.updatedb(self.name, Meta(self))
except Exception as e:
@ -323,18 +322,6 @@ class DocType(Document):
clear_linked_doctype_cache()
def delete_duplicate_custom_fields(self):
if not (frappe.db.table_exists(self.name) and frappe.db.table_exists("Custom Field")):
return
fields = [d.fieldname for d in self.fields if d.fieldtype in data_fieldtypes]
if fields:
frappe.db.sql('''delete from
`tabCustom Field`
where
dt = {0} and fieldname in ({1})
'''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True)
def sync_global_search(self):
'''If global search settings are changed, rebuild search properties for this table'''
global_search_fields_before_update = [d.fieldname for d in
@ -669,7 +656,7 @@ class DocType(Document):
flags = {"flags": re.ASCII} if six.PY3 else {}
# a DocType name should not start or end with an empty space
if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
@ -677,24 +664,24 @@ class DocType(Document):
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
def validate_links_table_fieldnames(self):
"""Validate fieldnames in Links table"""
if frappe.flags.in_patch: return
if frappe.flags.in_fixtures: return
if not self.links: return
for index, link in enumerate(self.links):
meta = frappe.get_meta(link.link_doctype)
if not meta.get_field(link.link_fieldname):
message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
validate_route_conflict(self.doctype, self.name)
def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""
if frappe.flags.in_patch: return
if frappe.flags.in_fixtures: return
if not meta.links: return
for index, link in enumerate(meta.links):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
def validate_fields_for_doctype(doctype):
doc = frappe.get_doc("DocType", doctype)
doc.delete_duplicate_custom_fields()
validate_fields(frappe.get_meta(doctype, cached=False))
meta = frappe.get_meta(doctype, cached=False)
validate_links_table_fieldnames(meta)
validate_fields(meta)
# this is separate because it is also called via custom field
def validate_fields(meta):

View file

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

View file

@ -5,12 +5,17 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMandatoryError, DoctypeLinkError, WrongOptionsDoctypeLinkError,\
HiddenAndMandatoryWithoutDefaultError, CannotIndexedError, InvalidFieldNameError, CannotCreateStandardDoctypeError
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
IllegalMandatoryError,
DoctypeLinkError,
WrongOptionsDoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
CannotIndexedError,
InvalidFieldNameError,
validate_links_table_fieldnames)
# test_records = frappe.get_test_records('DocType')
class TestDocType(unittest.TestCase):
def test_validate_name(self):
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
@ -459,7 +464,7 @@ class TestDocType(unittest.TestCase):
'link_doctype': "User",
'link_fieldname': "first_name"
})
doc.validate_links_table_fieldnames() # no error
validate_links_table_fieldnames(doc) # no error
doc.links = [] # reset links table
# check invalid doctype
@ -467,7 +472,7 @@ class TestDocType(unittest.TestCase):
'link_doctype': "User2",
'link_fieldname': "first_name"
})
self.assertRaises(frappe.DoesNotExistError, doc.validate_links_table_fieldnames)
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc)
doc.links = [] # reset links table
# check invalid fieldname
@ -475,7 +480,7 @@ class TestDocType(unittest.TestCase):
'link_doctype': "User",
'link_fieldname': "a_field_that_does_not_exists"
})
self.assertRaises(InvalidFieldNameError, doc.validate_links_table_fieldnames)
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
def new_doctype(name, unique=0, depends_on='', fields=None):

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

@ -36,7 +36,7 @@ def has_unseen_error_log(user):
def _get_response(show_alert=True):
return {
'show_alert': True,
'message': _("You have unseen {0}").format('<a href="/desk#List/Error%20Log/List"> Error Logs </a>')
'message': _("You have unseen {0}").format('<a href="/app/List/Error%20Log/List"> Error Logs </a>')
}
if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):

View file

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

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

@ -23,9 +23,9 @@ class NavbarSettings(Document):
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
@frappe.whitelist()
@frappe.whitelist(allow_guest=True)
def get_app_logo():
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo')
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
if not app_logo:
app_logo = frappe.get_hooks('app_logo_url')[-1]

View file

@ -9,6 +9,7 @@ from frappe.build import html_to_js_template
from frappe.model.utils import render_include
from frappe import conf, _, safe_decode
from frappe.desk.form.meta import get_code_files_via_hooks, get_js
from frappe.desk.utils import validate_route_conflict
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from six import text_type
@ -33,6 +34,8 @@ class Page(Document):
self.name += '-' + str(cnt)
def validate(self):
validate_route_conflict(self.doctype, self.name)
if self.is_new() and not getattr(conf,'developer_mode', 0):
frappe.throw(_("Not in Developer Mode"))

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

@ -0,0 +1,10 @@
import frappe
from ..role import desk_properties
def execute():
frappe.reload_doctype('role')
for role in frappe.get_all('Role', ['name', 'desk_access']):
role_doc = frappe.get_doc('Role', role.name)
for key in desk_properties:
role_doc.set(key, role_doc.desk_access)
role_doc.save()

View file

@ -13,7 +13,19 @@
"column_break_4",
"disabled",
"desk_access",
"two_factor_auth"
"two_factor_auth",
"navigation_settings_section",
"search_bar",
"notifications",
"chat",
"list_settings_section",
"list_sidebar",
"bulk_actions",
"view_switcher",
"form_settings_section",
"form_sidebar",
"timeline",
"dashboard"
],
"fields": [
{
@ -60,12 +72,82 @@
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "navigation_settings_section",
"fieldtype": "Section Break",
"label": "Navigation Settings"
},
{
"default": "1",
"fieldname": "search_bar",
"fieldtype": "Check",
"label": "Search Bar"
},
{
"default": "1",
"fieldname": "chat",
"fieldtype": "Check",
"label": "Chat"
},
{
"fieldname": "list_settings_section",
"fieldtype": "Section Break",
"label": "List Settings"
},
{
"default": "1",
"fieldname": "list_sidebar",
"fieldtype": "Check",
"label": "Sidebar"
},
{
"default": "1",
"fieldname": "bulk_actions",
"fieldtype": "Check",
"label": "Bulk Actions"
},
{
"fieldname": "form_settings_section",
"fieldtype": "Section Break",
"label": "Form Settings"
},
{
"default": "1",
"fieldname": "form_sidebar",
"fieldtype": "Check",
"label": "Sidebar"
},
{
"default": "1",
"fieldname": "timeline",
"fieldtype": "Check",
"label": "Timeline"
},
{
"default": "1",
"fieldname": "dashboard",
"fieldtype": "Check",
"label": "Dashboard"
},
{
"default": "1",
"fieldname": "view_switcher",
"fieldtype": "Check",
"label": "View Switcher"
},
{
"default": "1",
"fieldname": "notifications",
"fieldtype": "Check",
"label": "Notifications"
}
],
"icon": "fa fa-bookmark",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-06 15:42:59.036960",
"modified": "2020-12-03 14:08:38.181035",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",

View file

@ -6,6 +6,9 @@ import frappe
from frappe.model.document import Document
desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
class Role(Document):
def before_rename(self, old, new, merge=False):
if old in ("Guest", "Administrator", "System Manager", "All"):
@ -16,11 +19,28 @@ class Role(Document):
def validate(self):
if self.disabled:
if self.name in ("Guest", "Administrator", "System Manager", "All"):
frappe.throw(frappe._("Standard roles cannot be disabled"))
else:
frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
frappe.clear_cache()
self.disable_role()
else:
self.set_desk_properties()
def disable_role(self):
if self.name in ("Guest", "Administrator", "System Manager", "All"):
frappe.throw(frappe._("Standard roles cannot be disabled"))
else:
self.remove_roles()
def set_desk_properties(self):
# set if desk_access is not allowed, unset all desk properties
if self.name == 'Guest':
self.desk_access = 0
if not self.desk_access:
for key in desk_properties:
self.set(key, 0)
def remove_roles(self):
frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
frappe.clear_cache()
def on_update(self):
'''update system user desk access if this has changed in this update'''

View file

@ -3,32 +3,32 @@
frappe.ui.form.on('Role Permission for Page and Report', {
setup: function(frm) {
frm.trigger("set_queries")
frm.trigger("set_queries");
},
refresh: function(frm) {
frm.disable_save();
frm.role_area.hide();
frm.events.add_custom_buttons(frm);
frm.events.setup_buttons(frm);
},
add_custom_buttons: function(frm) {
setup_buttons: function(frm) {
frm.clear_custom_buttons();
if(frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) {
frm.page.clear_actions();
if (frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) {
frm.add_custom_button(__("Reset to defaults"), function() {
frm.trigger("reset_roles");
});
frm.add_custom_button(__("Update"), function() {
frm.page.set_primary_action(__("Update"), () => {
frm.trigger("update_report_page_data");
}).addClass('btn-primary');
});
}
},
onload: function(frm) {
if(!frm.roles_editor) {
frm.role_area = $('<div style="min-height: 300px">')
.appendTo(frm.fields_dict.roles_html.wrapper);
if (!frm.roles_editor) {
frm.role_area = $(frm.fields_dict.roles_html.wrapper);
frm.roles_editor = new frappe.RoleEditor(frm.role_area, frm);
}
},
@ -54,17 +54,17 @@ frappe.ui.form.on('Role Permission for Page and Report', {
},
page: function(frm) {
frm.events.add_custom_buttons(frm);
if(frm.doc.page) {
frm.events.setup_buttons(frm);
if (frm.doc.page) {
frm.trigger("set_report_page_data");
} else {
frm.trigger("set_role_for");
}
},
report: function(frm){
frm.events.add_custom_buttons(frm);
if(frm.doc.report) {
report: function(frm) {
frm.events.setup_buttons(frm);
if (frm.doc.report) {
frm.trigger("set_report_page_data");
} else {
frm.trigger("set_role_for");

View file

@ -3,20 +3,18 @@
frappe.ui.form.on('Role Profile', {
refresh: function(frm) {
if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if(!frm.roles_editor) {
var role_area = $('<div style="min-height: 300px">')
.appendTo(frm.fields_dict.roles_html.wrapper);
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if (!frm.roles_editor) {
const role_area = $(frm.fields_dict.roles_html.wrapper);
frm.roles_editor = new frappe.RoleEditor(role_area, frm);
frm.roles_editor.show();
} else {
frm.roles_editor.show();
}
frm.roles_editor.show();
}
},
validate: function(frm) {
if(frm.roles_editor) {
if (frm.roles_editor) {
frm.roles_editor.set_roles_in_table();
}
}

View file

@ -3,6 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
from typing import Dict, List
import frappe, json
from frappe.model.document import Document
@ -11,12 +12,13 @@ from datetime import datetime
from croniter import croniter
from frappe.utils.background_jobs import enqueue, get_jobs
class ScheduledJobType(Document):
def autoname(self):
self.name = '.'.join(self.method.split('.')[-2:])
self.name = ".".join(self.method.split(".")[-2:])
def validate(self):
if self.frequency != 'All':
if self.frequency != "All":
# force logging for all events other than continuous ones (ALL)
self.create_log = 1
@ -84,7 +86,7 @@ class ScheduledJobType(Document):
def log_status(self, status):
# log file
frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
frappe.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {frappe.local.site}")
self.update_scheduler_log(status)
def update_scheduler_log(self, status):
@ -111,28 +113,28 @@ class ScheduledJobType(Document):
@frappe.whitelist()
def execute_event(doc):
frappe.only_for('System Manager')
def execute_event(doc: str):
frappe.only_for("System Manager")
doc = json.loads(doc)
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True)
frappe.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True)
def run_scheduled_job(job_type):
'''This is a wrapper function that runs a hooks.scheduler_events method'''
def run_scheduled_job(job_type: str):
"""This is a wrapper function that runs a hooks.scheduler_events method"""
try:
frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute()
frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute()
except Exception:
print(frappe.get_traceback())
def sync_jobs(hooks=None):
def sync_jobs(hooks: Dict = None):
frappe.reload_doc("core", "doctype", "scheduled_job_type")
scheduler_events = hooks or frappe.get_hooks("scheduler_events")
all_events = insert_events(scheduler_events)
clear_events(all_events)
def insert_events(scheduler_events):
def insert_events(scheduler_events: Dict) -> List:
cron_jobs, event_jobs = [], []
for event_type in scheduler_events:
events = scheduler_events.get(event_type)
@ -144,7 +146,7 @@ def insert_events(scheduler_events):
return cron_jobs + event_jobs
def insert_cron_jobs(events):
def insert_cron_jobs(events: Dict) -> List:
cron_jobs = []
for cron_format in events:
for event in events.get(cron_format):
@ -153,25 +155,29 @@ def insert_cron_jobs(events):
return cron_jobs
def insert_event_jobs(events, event_type):
def insert_event_jobs(events: List, event_type: str) -> List:
event_jobs = []
for event in events:
event_jobs.append(event)
frequency = event_type.replace('_', ' ').title()
frequency = event_type.replace("_", " ").title()
insert_single_event(frequency, event)
return event_jobs
def insert_single_event(frequency, event, cron_format=None):
def insert_single_event(frequency: str, event: str, cron_format: str = None):
cron_expr = {"cron_format": cron_format} if cron_format else {}
doc = frappe.get_doc({
"doctype": "Scheduled Job Type",
"method": event,
"cron_format": cron_format,
"frequency": frequency
})
doc = frappe.get_doc(
{
"doctype": "Scheduled Job Type",
"method": event,
"cron_format": cron_format,
"frequency": frequency,
}
)
if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }):
if not frappe.db.exists(
"Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr}
):
try:
doc.insert()
except frappe.DuplicateEntryError:
@ -179,7 +185,12 @@ def insert_single_event(frequency, event, cron_format=None):
doc.insert()
def clear_events(all_events):
for event in frappe.get_all("Scheduled Job Type", ("name", "method")):
if event.method not in all_events:
def clear_events(all_events: List):
for event in frappe.get_all(
"Scheduled Job Type", fields=["name", "method", "server_script"]
):
is_server_script = event.server_script
is_defined_in_hooks = event.method in all_events
if not (is_defined_in_hooks or is_server_script):
frappe.delete_doc("Scheduled Job Type", event.name)

View file

@ -6,46 +6,11 @@ frappe.ui.form.on('Server Script', {
frm.trigger('setup_help');
},
refresh: function(frm) {
if (frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled) {
frm.add_custom_button('Schedule Script', function() {
var d = new frappe.ui.Dialog({
title: "Schedule Script Execution",
fields: [
{
fieldname: "event_type",
label: __('Select Event Type'),
fieldtype: "Select",
options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
},
],
primary_action_label: __('Schedule Script'),
primary_action: () => {
d.get_primary_btn().attr('disabled', true);
var data = d.get_values();
d.hide();
if(data) {
frm.events.schedule_script(frm, data);
}
}
});
d.show();
});
if (frm.doc.script_type != 'Scheduler Event') {
frm.dashboard.hide();
}
},
schedule_script(frm, data) {
frm.call({
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
args: {
'script_name': frm.doc.name,
'frequency': data.event_type
}
});
},
setup_help(frm) {
frm.get_field('help_html').html(`
<h4>DocType Event</h4>

View file

@ -8,6 +8,7 @@
"field_order": [
"script_type",
"reference_doctype",
"event_frequency",
"doctype_event",
"api_method",
"allow_guest",
@ -84,11 +85,24 @@
{
"fieldname": "help_html",
"fieldtype": "HTML"
},
{
"depends_on": "eval:doc.script_type == \"Scheduler Event\"",
"fieldname": "event_frequency",
"fieldtype": "Select",
"label": "Event Frequency",
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-03 18:50:14.767595",
"links": [
{
"link_doctype": "Scheduled Job Type",
"link_fieldname": "server_script"
}
],
"modified": "2021-02-18 12:36:19.803425",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import ast
from typing import Dict, List
import frappe
from frappe.model.document import Document
@ -14,67 +15,146 @@ from frappe import _
class ServerScript(Document):
def validate(self):
frappe.only_for('Script Manager', True)
frappe.only_for("Script Manager", True)
self.validate_script()
self.sync_scheduled_jobs()
self.clear_scheduled_events()
def on_update(self):
frappe.cache().delete_value("server_script_map")
self.sync_scheduler_events()
def on_trash(self):
if self.script_type == "Scheduler Event":
for job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", job.name)
@property
def scheduled_jobs(self) -> List[Dict[str, str]]:
return frappe.get_all(
"Scheduled Job Type",
filters={"server_script": self.name},
fields=["name", "stopped"],
)
def validate_script(self):
"""Utilizes the ast module to check for syntax errors
"""
ast.parse(self.script)
@staticmethod
def on_update():
frappe.cache().delete_value('server_script_map')
def sync_scheduled_jobs(self):
"""Sync Scheduled Job Type statuses if Server Script's disabled status is changed
"""
if self.script_type != "Scheduler Event" or not self.has_value_changed("disabled"):
return
def execute_method(self):
if self.script_type == 'API':
# validate if guest is allowed
if frappe.session.user == 'Guest' and not self.allow_guest:
raise frappe.PermissionError
_globals, _locals = safe_exec(self.script)
return _globals.frappe.flags # output can be stored in flags
else:
# wrong report type!
for scheduled_job in self.scheduled_jobs:
if bool(scheduled_job.stopped) != bool(self.disabled):
job = frappe.get_doc("Scheduled Job Type", scheduled_job.name)
job.stopped = self.disabled
job.save()
def sync_scheduler_events(self):
"""Create or update Scheduled Job Type documents for Scheduler Event Server Scripts
"""
if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event":
setup_scheduler_events(script_name=self.name, frequency=self.event_frequency)
def clear_scheduled_events(self):
"""Deletes existing scheduled jobs by Server Script if self.event_frequency has changed
"""
if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"):
for scheduled_job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", scheduled_job.name)
def execute_method(self) -> Dict:
"""Specific to API endpoint Server Scripts
Raises:
frappe.DoesNotExistError: If self.script_type is not API
frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user
Returns:
dict: Evaluates self.script with frappe.utils.safe_exec.safe_exec and returns the flags set in it's safe globals
"""
# wrong report type!
if self.script_type != "API":
raise frappe.DoesNotExistError
def execute_doc(self, doc):
# execute event
safe_exec(self.script, None, dict(doc = doc))
# validate if guest is allowed
if frappe.session.user == "Guest" and not self.allow_guest:
raise frappe.PermissionError
# output can be stored in flags
_globals, _locals = safe_exec(self.script)
return _globals.frappe.flags
def execute_doc(self, doc: Document):
"""Specific to Document Event triggered Server Scripts
Args:
doc (Document): Executes script with for a certain document's events
"""
safe_exec(self.script, _locals={"doc": doc})
def execute_scheduled_method(self):
if self.script_type == 'Scheduler Event':
safe_exec(self.script)
else:
# wrong report type!
"""Specific to Scheduled Jobs via Server Scripts
Raises:
frappe.DoesNotExistError: If script type is not a scheduler event
"""
if self.script_type != "Scheduler Event":
raise frappe.DoesNotExistError
def get_permission_query_conditions(self, user):
safe_exec(self.script)
def get_permission_query_conditions(self, user: str) -> List[str]:
"""Specific to Permission Query Server Scripts
Args:
user (str): Takes user email to execute script and return list of conditions
Returns:
list: Returns list of conditions defined by rules in self.script
"""
locals = {"user": user, "conditions": ""}
safe_exec(self.script, None, locals)
if locals["conditions"]:
return locals["conditions"]
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub('{0}-{1}'.format(script_name, frequency))
scheduled_script = frappe.db.get_value('Scheduled Job Type',
dict(method=method))
"""Creates or Updates Scheduled Job Type documents based on the specified script name and frequency
Args:
script_name (str): Name of the Server Script document
frequency (str): Event label compatible with the Frappe scheduler
"""
method = frappe.scrub(f"{script_name}-{frequency}")
scheduled_script = frappe.db.get_value("Scheduled Job Type", {"method": method})
if not scheduled_script:
doc = frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = method,
frequency = frequency,
server_script = script_name
))
frappe.get_doc(
{
"doctype": "Scheduled Job Type",
"method": method,
"frequency": frequency,
"server_script": script_name,
}
).insert()
doc.insert()
frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name))
frappe.msgprint(_("Enabled scheduled execution for script {0}").format(script_name))
else:
doc = frappe.get_doc('Scheduled Job Type', scheduled_script)
doc.update(dict(
doctype = 'Scheduled Job Type',
method = method,
frequency = frequency,
server_script = script_name
))
doc = frappe.get_doc("Scheduled Job Type", scheduled_script)
if doc.frequency == frequency:
return
doc.frequency = frequency
doc.save()
frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name))
frappe.msgprint(
_("Scheduled execution for script {0} has updated").format(script_name)
)

View file

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

View file

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"localization",
"app_name",
"country",
"language",
"column_break_3",
@ -462,6 +463,19 @@
"fieldtype": "Section Break",
"label": "Prepared Report"
},
{
"default": "Frappe",
"fieldname": "app_name",
"fieldtype": "Data",
"label": "App Name"
},
{
"default": "3",
"description": "Hourly rate limit for generating password reset links",
"fieldname": "password_reset_limit",
"fieldtype": "Int",
"label": "Password Reset Link Generation Limit"
},
{
"default": "1",
"fieldname": "strip_exif_metadata_from_uploaded_images",
@ -472,7 +486,7 @@
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2020-11-30 18:52:22.161391",
"modified": "2020-12-30 18:52:22.161391",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -2,7 +2,7 @@
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, unittest
import frappe, unittest, uuid
from frappe.model.delete_doc import delete_doc
from frappe.utils.data import today, add_to_date
@ -20,6 +20,7 @@ class TestUser(unittest.TestCase):
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", "")
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 3)
frappe.set_user('Administrator')
def test_user_type(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com',
@ -106,13 +107,17 @@ class TestUser(unittest.TestCase):
frappe.set_user("testperm@example.com")
me = frappe.get_doc("User", "testperm@example.com")
self.assertRaises(frappe.PermissionError, me.add_roles, "System Manager")
me.add_roles("System Manager")
# system manager is not added (it is reset)
self.assertFalse('System Manager' in [d.role for d in me.roles])
frappe.set_user("Administrator")
me = frappe.get_doc("User", "testperm@example.com")
me.add_roles("System Manager")
# system manager now added by Administrator
self.assertTrue("System Manager" in [d.role for d in me.get("roles")])
# def test_deny_multiple_sessions(self):
@ -235,6 +240,29 @@ class TestUser(unittest.TestCase):
self.assertRaises(frappe.ValidationError, user.reset_password, False)
def test_user_rollback(self):
""" """
frappe.db.commit()
frappe.db.begin()
user_id = str(uuid.uuid4())
email = f'{user_id}@example.com'
try:
frappe.flags.in_import = True # disable throttling
frappe.get_doc(dict(
doctype='User',
email=email,
first_name=user_id,
)).insert()
finally:
frappe.flags.in_import = False
# Check user has been added
self.assertIsNotNone(frappe.db.get("User", {"email": email}))
# Check that rollback works
frappe.db.rollback()
self.assertIsNone(frappe.db.get("User", {"email": email}))
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)

View file

@ -1,28 +0,0 @@
.user-role {
padding: 5px;
width: 50%;
float: left;
}
table.user-perm {
border-collapse: collapse;
width: 100%;
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
table.user-perm td, table.user-perm th {
padding: 5px;
text-align: center;
border-bottom: 1px solid #aaa;
min-width: 30px;
}
.module-block-list .checkbox {
margin-bottom: 0px;
}
.module-block-list .checkbox label {
width: 100%;
}

View file

@ -27,7 +27,7 @@ frappe.ui.form.on('User', {
},
callback: function(data) {
frm.set_value("roles", []);
$.each(data.message || [], function(i, v){
$.each(data.message || [], function(i, v) {
var d = frm.add_child("roles");
d.role = v.role;
});
@ -37,16 +37,35 @@ frappe.ui.form.on('User', {
}
},
module_profile: function(frm) {
if (frm.doc.module_profile) {
frappe.call({
"method": "frappe.core.doctype.user.user.get_module_profile",
args: {
module_profile: frm.doc.module_profile
},
callback: function(data) {
frm.set_value("block_modules", []);
$.each(data.message || [], function(i, v) {
let d = frm.add_child("block_modules");
d.module = v.module;
});
frm.module_editor && frm.module_editor.refresh();
}
});
}
},
onload: function(frm) {
frm.can_edit_roles = has_access_to_edit_user();
if(frm.can_edit_roles && !frm.is_new()) {
if(!frm.roles_editor) {
var role_area = $('<div style="min-height: 300px">')
if (frm.can_edit_roles && !frm.is_new()) {
if (!frm.roles_editor) {
const role_area = $('<div class="role-editor">')
.appendTo(frm.fields_dict.roles_html.wrapper);
frm.roles_editor = new frappe.RoleEditor(role_area, frm, frm.doc.role_profile_name ? 1 : 0);
var module_area = $('<div style="min-height: 300px">')
var module_area = $('<div>')
.appendTo(frm.fields_dict.modules_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
} else {
@ -255,43 +274,3 @@ function get_roles_for_editing_user() {
.filter(perm => perm.permlevel >= 1 && perm.write)
.map(perm => perm.role) || ['System Manager'];
}
frappe.ModuleEditor = Class.extend({
init: function(frm, wrapper) {
this.wrapper = $('<div class="row module-block-list"></div>').appendTo(wrapper);
this.frm = frm;
this.make();
},
make: function() {
var me = this;
this.frm.doc.__onload.all_modules.forEach(function(m) {
$(repl('<div class="col-sm-6"><div class="checkbox">\
<label><input type="checkbox" class="block-module-check" data-module="%(module)s">\
%(module)s</label></div></div>', {module: m})).appendTo(me.wrapper);
});
this.bind();
},
refresh: function() {
var me = this;
this.wrapper.find(".block-module-check").prop("checked", true);
$.each(this.frm.doc.block_modules, function(i, d) {
me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false);
});
},
bind: function() {
var me = this;
this.wrapper.on("change", ".block-module-check", function() {
var module = $(this).attr('data-module');
if($(this).prop("checked")) {
// remove from block_modules
me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) {
if (d.module != module) {
return d;
}
});
} else {
me.frm.add_child("block_modules", {"module": module});
}
});
}
});

View file

@ -7,20 +7,20 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"sb0_5",
"enabled",
"section_break_3",
"email",
"first_name",
"middle_name",
"last_name",
"language",
"column_break0",
"first_name",
"full_name",
"time_zone",
"column_break_11",
"middle_name",
"username",
"send_welcome_email",
"unsubscribed",
"column_break0",
"username",
"language",
"time_zone",
"user_image",
"sb1",
"role_profile_name",
@ -28,15 +28,17 @@
"roles",
"short_bio",
"gender",
"phone",
"mobile_no",
"birth_date",
"location",
"banner_image",
"column_break_22",
"interest",
"banner_image",
"desk_theme",
"column_break_26",
"phone",
"location",
"bio",
"mute_sounds",
"column_break_22",
"mobile_no",
"change_password",
"new_password",
"logout_all_sessions",
@ -47,13 +49,13 @@
"document_follow_notify",
"document_follow_frequency",
"email_settings",
"email_signature",
"thread_notify",
"send_me_a_copy",
"allowed_in_mentions",
"email_signature",
"email_inbox",
"user_emails",
"sb_allow_modules",
"module_profile",
"modules_html",
"block_modules",
"home_settings",
@ -61,15 +63,16 @@
"defaults",
"sb3",
"simultaneous_sessions",
"user_type",
"login_after",
"login_before",
"restrict_ip",
"bypass_restrict_ip_check_if_2fa_enabled",
"column_break1",
"last_login",
"last_ip",
"column_break1",
"login_after",
"user_type",
"last_active",
"section_break_63",
"login_before",
"bypass_restrict_ip_check_if_2fa_enabled",
"last_login",
"last_known_versions",
"third_party_authentication",
"social_logins",
@ -80,10 +83,6 @@
"api_secret"
],
"fields": [
{
"fieldname": "sb0_5",
"fieldtype": "Section Break"
},
{
"default": "1",
"fieldname": "enabled",
@ -96,7 +95,8 @@
{
"depends_on": "enabled",
"fieldname": "section_break_3",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Basic Info"
},
{
"fieldname": "email",
@ -302,7 +302,7 @@
"no_copy": 1
},
{
"default": "0",
"default": "1",
"fieldname": "logout_all_sessions",
"fieldtype": "Check",
"label": "Logout From All Devices After Changing Password"
@ -577,6 +577,30 @@
"fieldtype": "Password",
"label": "API Secret",
"read_only": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_63",
"fieldtype": "Column Break"
},
{
"fieldname": "desk_theme",
"fieldtype": "Select",
"label": "Desk Theme",
"options": "Light\nDark"
},
{
"fieldname": "module_profile",
"fieldtype": "Link",
"label": "Module Profile",
"options": "Module Profile"
}
],
"icon": "fa fa-user",
@ -623,11 +647,6 @@
"link_doctype": "User Permission",
"link_fieldname": "user"
},
{
"group": "Settings",
"link_doctype": "Assignment Rule",
"link_fieldname": "user"
},
{
"group": "Settings",
"link_doctype": "Document Follow",
@ -650,7 +669,7 @@
}
],
"max_attachments": 5,
"modified": "2020-10-18 15:18:53.126800",
"modified": "2021-02-01 16:11:06.037543",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@ -678,10 +697,11 @@
}
],
"quick_entry": 1,
"route": "user",
"search_fields": "full_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
}
}

View file

@ -75,6 +75,7 @@ class User(Document):
self.validate_user_email_inbox()
ask_pass_update()
self.validate_roles()
self.validate_allowed_modules()
self.validate_user_image()
if self.language == "Loading...":
@ -85,9 +86,18 @@ class User(Document):
def validate_roles(self):
if self.role_profile_name:
role_profile = frappe.get_doc('Role Profile', self.role_profile_name)
self.set('roles', [])
self.append_roles(*[role.role for role in role_profile.roles])
role_profile = frappe.get_doc('Role Profile', self.role_profile_name)
self.set('roles', [])
self.append_roles(*[role.role for role in role_profile.roles])
def validate_allowed_modules(self):
if self.module_profile:
module_profile = frappe.get_doc('Module Profile', self.module_profile)
self.set('block_modules', [])
for d in module_profile.get('block_modules'):
self.append('block_modules', {
'module': d.module
})
def validate_user_image(self):
if self.user_image and len(self.user_image) > 2000:
@ -108,7 +118,7 @@ class User(Document):
)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now)
# Set user selected timezone
if self.time_zone:
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
@ -187,20 +197,17 @@ class User(Document):
def share_with_self(self):
if self.user_type=="System User":
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
flags={"ignore_share_permission": True})
else:
frappe.share.remove(self.doctype, self.name, self.name,
flags={"ignore_share_permission": True, "ignore_permissions": True})
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
flags={"ignore_share_permission": True})
def validate_share(self, docshare):
if docshare.user == self.name:
if self.user_type=="System User":
if docshare.share != 1:
frappe.throw(_("Sorry! User should have complete access to their own record."))
else:
frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
pass
# if docshare.user == self.name:
# if self.user_type=="System User":
# if docshare.share != 1:
# frappe.throw(_("Sorry! User should have complete access to their own record."))
# else:
# frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
def send_password_notification(self, new_password):
try:
@ -292,16 +299,16 @@ class User(Document):
from frappe.utils.user import get_user_fullname
from frappe.utils import get_url
full_name = get_user_fullname(frappe.session['user'])
if full_name == "Guest":
full_name = "Administrator"
created_by = get_user_fullname(frappe.session['user'])
if created_by == "Guest":
created_by = "Administrator"
args = {
'first_name': self.first_name or self.last_name or "user",
'user': self.name,
'title': subject,
'login_url': get_url(),
'user_fullname': full_name
'created_by': created_by
}
args.update(add_args)
@ -555,6 +562,10 @@ def get_perm_info(role):
@frappe.whitelist(allow_guest=True)
def update_password(new_password, logout_all_sessions=0, key=None, old_password=None):
#validate key to avoid key input like ['like', '%'], '', ['in', ['']]
if key and not isinstance(key, str):
frappe.throw(_('Invalid key type'))
result = test_password_strength(new_password, key, old_password)
feedback = result.get("feedback", None)
@ -585,7 +596,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
frappe.db.set_value("User", user, "reset_password_key", "")
if user_doc.user_type == "System User":
return "/desk"
return "/app"
else:
return redirect_url if redirect_url else "/"
@ -1006,9 +1017,14 @@ def send_token_via_email(tmp_id,token=None):
hotp = pyotp.HOTP(otpsecret)
frappe.sendmail(
recipients=user_email, sender=None, subject='Verification Code',
message='<p>Your verification code is {0}</p>'.format(hotp.at(int(count))),
delayed=False, retry=3)
recipients=user_email,
sender=None,
subject="Verification Code",
template="verification_code",
args=dict(code=hotp.at(int(count))),
delayed=False,
retry=3
)
return True
@ -1042,6 +1058,11 @@ def get_role_profile(role_profile):
roles = frappe.get_doc('Role Profile', {'role_profile': role_profile})
return roles.roles
@frappe.whitelist()
def get_module_profile(module_profile):
module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile})
return module_profile.get('block_modules')
def update_roles(role_profile):
users = frappe.get_all('User', filters={'role_profile_name': role_profile})
role_profile = frappe.get_doc('Role Profile', role_profile)
@ -1102,7 +1123,6 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
contact.save(ignore_permissions=True)
@frappe.whitelist()
def generate_keys(user):
"""
@ -1123,6 +1143,11 @@ def generate_keys(user):
return {"api_secret": api_secret}
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
@frappe.whitelist()
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
def update_password_reset_limit(user):
generated_link_count = get_generated_link_count(user)
generated_link_count += 1

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

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