Merge branch 'develop' into patch-attach-develop

This commit is contained in:
Prssanna Desai 2021-03-04 20:10:23 +05:30 committed by GitHub
commit b38c051a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1103 changed files with 46637 additions and 49500 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"

14
.editorconfig Normal file
View file

@ -0,0 +1,14 @@
# Root editor config file
root = true
# Common settings
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
[{*.py,*.js}]
indent_style = tab
indent_size = 4

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

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.erpnext.com/
about: For general QnA, discussions and community help.

View file

@ -4,7 +4,7 @@ from urllib.parse import urlparse
docs_repos = [
"frappe_docs"
"frappe_docs",
"erpnext_documentation",
"erpnext_com",
"frappe_io",
@ -17,12 +17,12 @@ def uri_validator(x):
def docs_link_exists(body):
for line in body.splitlines():
for word in line:
for word in line.split():
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
_, org, repo, _type, ref = parsed_url.path.split('/')
if org == "frappe" and repo in docs_repos:
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True

View file

@ -2,8 +2,11 @@ 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)*)*\)")
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
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[\"']")
starts_with_f_pattern = re.compile(r"_\(f")
# skip first argument
files = sys.argv[1:]
@ -14,21 +17,44 @@ for _file in files_to_scan:
print(f'Checking: {_file}')
file_lines = f.readlines()
for line_number, line in enumerate(file_lines, 1):
if 'frappe-lint: disable-translate' in line:
continue
start_matches = start_pattern.search(line)
if start_matches:
starts_with_f = starts_with_f_pattern.search(line)
if starts_with_f:
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}\n{line.strip()[:100]}')
continue
else:
continue
match = pattern.search(line)
if not match and line.endswith(',\n'):
error_found = False
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:]
match = pattern.match(line)
if not match:
error_found = True
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}\n{line.strip()[:100]}')
if error_found:
errors_encounter += 1
print(f'\nTranslation syntax error at line number: {line_number + 1}\n{line.strip()[:100]}')
if errors_encounter > 0:
print('\nYou can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
sys.exit(1)
else:
print('\nGood To Go!')

View file

@ -1,9 +1,10 @@
name: Trigger Docker build on release
name: 'Trigger Docker build on release'
on:
release:
types: [released]
jobs:
curl:
name: 'Trigger Docker build on release'
runs-on: ubuntu-latest
container:
image: alpine:latest

View file

@ -1,10 +1,11 @@
name: 'Documentation Required'
name: 'Documentation Check'
on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
jobs:
build:
docs-required:
name: 'Documentation Required'
runs-on: ubuntu-latest
steps:

View file

@ -1,11 +1,12 @@
name: Build and Publish Assets for Development
name: 'Frappe Assets'
on:
push:
branches: [ develop ]
jobs:
build:
build-dev-and-publish:
name: 'Build and Publish Assets for Development'
runs-on: ubuntu-latest
steps:

View file

@ -1,4 +1,4 @@
name: Build and Publish Assets built for Releases
name: 'Frappe Assets'
on:
release:
@ -8,7 +8,8 @@ env:
GITHUB_TOKEN: ${{ github.token }}
jobs:
build:
build-release-and-publish:
name: 'Build and Publish Assets built for Releases'
runs-on: ubuntu-latest
steps:
@ -44,4 +45,3 @@ jobs:
asset_path: build/assets.tar.gz
asset_name: assets.tar.gz
asset_content_type: application/octet-stream

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

@ -5,7 +5,7 @@ pull_request_rules:
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk (frappe)
- label!=don't-merge
- label!=dont-merge
- label!=squash
- "#approved-reviews-by>=1"
actions:
@ -14,10 +14,9 @@ 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!=don't-merge
- label!=dont-merge
- label=squash
- "#approved-reviews-by>=1"
actions:

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,14 +4,14 @@
# the repo. Unless a later match takes precedence,
* @frappe/frappe-review-team
website/ @scmmishra
web_form/ @scmmishra
templates/ @scmmishra
www/ @scmmishra
integrations/ @Mangesh-Khairnar
patches/ @sahil28297
website/ @prssanna
web_form/ @prssanna
templates/ @surajshetty3416
www/ @surajshetty3416
integrations/ @nextchamp-saqib
patches/ @surajshetty3416
dashboard/ @prssanna
email/ @Thunderbottom
email/ @saurabh6790
event_streaming/ @ruchamahabal
data_import* @netchampfaris
core/ @surajshetty3416

View file

@ -43,7 +43,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
## Contributing
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Pull-Request-Guidelines)
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Translations](https://translate.erpnext.com)
### Website

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

@ -0,0 +1,36 @@
context('Control Select', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
function get_dialog_with_select() {
return cy.dialog({
title: 'Select',
fields: [{
'fieldname': 'select_control',
'fieldtype': 'Select',
'placeholder': 'Select an Option',
'options': ['', 'Option 1', 'Option 2', 'Option 2'],
}]
});
}
it('toggles placholder on clicking an option', () => {
get_dialog_with_select().as('dialog');
cy.get('.frappe-control[data-fieldname=select_control] .control-input').as('control');
cy.get('.frappe-control[data-fieldname=select_control] .control-input select').as('select');
cy.get('@control').get('.select-icon').should('exist');
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
cy.get('@select').select('Option 1');
cy.get('@control').get('.placeholder').should('have.css', 'display', 'none');
cy.get('@select').invoke('val', '');
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
cy.get('@dialog').then(dialog => {
dialog.hide();
});
});
});

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

View file

@ -1,7 +1,7 @@
context('FileUploader', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
cy.visit('/app');
});
function open_upload_dialog() {
@ -19,40 +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' },
);
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

@ -8,20 +8,19 @@ context('Table MultiSelect', () => {
it('select value from multiselect dropdown', () => {
cy.new_form('Assignment Rule');
cy.fill_field('__newname', name);
cy.fill_field('document_type', 'ToDo');
cy.fill_field('document_type', 'Blog Post');
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
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

@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
Cypress.Commands.add('create_records', doc => {
return cy
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
.then(r => r.message);
});
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false, force: true });
cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
@ -204,19 +204,54 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
return cy.get(selector);
});
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
cy.get('@input').click().wait(200);
cy.get('.datepickers-container .datepicker.active').should('exist');
}
if (fieldtype === 'Time') {
cy.get('@input').clear().wait(200);
}
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
selector += ` [data-idx="${row_idx}"]`;
selector += ` .form-in-grid`;
if (fieldtype === 'Text Editor') {
selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
} else if (fieldtype === 'Code') {
selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
} else {
selector += ` .form-control[data-fieldname="${fieldname}"]`;
}
return cy.get(selector);
});
Cypress.Commands.add('awesomebar', text => {
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
});
Cypress.Commands.add('new_form', doctype => {
let 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', () => {
@ -240,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');
});
@ -272,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
@ -11,7 +17,6 @@ from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
from past.builtins import cmp
import click
from faker import Faker
# public
from .exceptions import *
@ -27,6 +32,7 @@ __version__ = '13.0.0-dev'
__title__ = "Frappe Framework"
local = Local()
controllers = {}
class _dict(dict):
"""dict like object that exposes keys as attributes"""
@ -148,6 +154,7 @@ def init(site, sites_path=None, new_site=False):
"new_site": new_site
})
local.rollback_observers = []
local.before_commit = []
local.test_objects = {}
local.site = site
@ -188,17 +195,20 @@ def init(site, sites_path=None, new_site=False):
local.initialised = True
def connect(site=None, db_name=None):
def connect(site=None, db_name=None, set_admin_as_user=True):
"""Connect to site database instance.
:param site: If site is given, calls `frappe.init`.
:param db_name: Optional. Will use from `site_config.json`."""
:param db_name: Optional. Will use from `site_config.json`.
:param set_admin_as_user: Set Administrator as current user.
"""
from frappe.database import get_db
if site:
init(site)
local.db = get_db(user=db_name or local.conf.db_name)
set_user("Administrator")
if set_admin_as_user:
set_user("Administrator")
def connect_replica():
from frappe.database import get_db
@ -312,7 +322,7 @@ def log(msg):
debug_log.append(as_unicode(msg))
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
response JSON and shown in a pop-up / modal.
@ -321,11 +331,12 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
:param title: [optional] Message title.
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
"""
from frappe.utils import encode
from frappe.utils import strip_html_tags
msg = safe_decode(msg)
out = _dict(message=msg)
@ -346,19 +357,13 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
return
if as_table and type(msg) in (list, tuple):
out.as_table = 1
table_rows = ''
for row in msg:
table_row_data = ''
for data in row:
table_row_data += '<td>{}</td>'.format(data)
table_rows += '<tr>{}</tr>'.format(table_row_data)
out.message = '''<table class="table table-bordered"
style="margin: 0;">{}</table>'''.format(table_rows)
if as_list and type(msg) in (list, tuple) and len(msg) > 1:
out.as_list = 1
if flags.print_messages and out.message:
print(f"Message: {repr(out.message).encode('utf-8')}")
print(f"Message: {strip_html_tags(out.message)}")
if title:
out.title = title
@ -405,12 +410,12 @@ def clear_last_message():
if len(local.message_log) > 0:
local.message_log = local.message_log[:-1]
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None):
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, as_list=False):
"""Throw execption and show message (`msgprint`).
:param msg: Message.
:param exc: Exception class. Default `frappe.ValidationError`"""
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide)
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list)
def emit_js(js, user=False, **kwargs):
if user == False:
@ -465,11 +470,11 @@ def get_request_header(key, default=None):
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message",
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, content=None, doctype=None, name=None, reply_to=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1,
attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False,
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**.
@ -495,6 +500,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:
@ -512,12 +518,12 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
from frappe.email import queue
queue.send(recipients=recipients, sender=sender,
subject=subject, message=message, text_content=text_content,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_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,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
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 = []
@ -632,6 +638,21 @@ def clear_cache(user=None, doctype=None):
local.role_permissions = {}
def only_has_select_perm(doctype, user=None, ignore_permissions=False):
if ignore_permissions:
return False
if not user:
user = local.session.user
import frappe.permissions
permissions = frappe.permissions.get_role_permissions(doctype, user=user)
if permissions.get('select') and not permissions.get('read'):
return True
else:
return False
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
"""Raises `frappe.PermissionError` if not permitted.
@ -801,11 +822,17 @@ def get_doc(*args, **kwargs):
return doc
def get_last_doc(doctype):
def get_last_doc(doctype, filters=None, order_by="creation desc"):
"""Get last created document of this type."""
d = get_all(doctype, ["name"], order_by="creation desc", limit_page_length=1)
d = get_all(
doctype,
filters=filters,
limit_page_length=1,
order_by=order_by,
pluck="name"
)
if d:
return get_doc(doctype, d[0].name)
return get_doc(doctype, d[0])
else:
raise DoesNotExistError
@ -944,7 +971,7 @@ def get_installed_apps(sort=False, frappe_last=False):
connect()
if not local.all_apps:
local.all_apps = get_all_apps(True)
local.all_apps = cache().get_value('all_apps', get_all_apps)
installed = json.loads(db.get_global("installed_apps") or "[]")
@ -1159,6 +1186,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
'doctype_or_field': args.doctype_or_field,
'doc_type': doctype,
'field_name': args.fieldname,
'row_name': args.row_name,
'property': args.property,
'value': args.value,
'property_type': args.property_type or "Data",
@ -1609,7 +1637,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,
@ -1721,6 +1749,8 @@ def parse_json(val):
return parse_json(val)
def mock(type, size=1, locale='en'):
from faker import Faker
results = []
faker = Faker(locale)
if not type in dir(faker):

View file

@ -7,8 +7,8 @@ import os
from six import iteritems
import logging
from werkzeug.wrappers import Request
from werkzeug.local import LocalManager
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware
@ -57,19 +57,22 @@ def application(request):
frappe.monitor.start()
frappe.rate_limiter.apply()
if frappe.local.form_dict.cmd:
if request.method == "OPTIONS":
response = Response()
elif frappe.form_dict.cmd:
response = frappe.handler.handle()
elif frappe.request.path.startswith("/api/"):
elif request.path.startswith("/api/"):
response = frappe.api.handle()
elif frappe.request.path.startswith('/backups'):
elif request.path.startswith('/backups'):
response = frappe.utils.response.download_backup(request.path)
elif frappe.request.path.startswith('/private/files/'):
elif request.path.startswith('/private/files/'):
response = frappe.utils.response.download_private_file(request.path)
elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()
else:
@ -88,13 +91,9 @@ def application(request):
rollback = after_request(rollback)
finally:
if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
if request.method in ("POST", "PUT") and frappe.db and rollback:
frappe.db.rollback()
# set cookies
if response and hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
@ -110,9 +109,7 @@ def application(request):
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if response and hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
process_response(response)
frappe.destroy()
return response
@ -131,10 +128,51 @@ def init_request(request):
if frappe.local.conf.get('maintenance_mode'):
frappe.connect()
raise frappe.SessionStopped('Session Stopped')
else:
frappe.connect(set_admin_as_user=False)
make_form_dict(request)
frappe.local.http_request = frappe.auth.HTTPRequest()
if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest()
def process_response(response):
if not response:
return
# set cookies
if hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
# rate limiter headers
if hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
# CORS headers
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
set_cors_headers(response)
def set_cors_headers(response):
origin = frappe.request.headers.get('Origin')
allow_cors = frappe.conf.allow_cors
if not (origin and allow_cors):
return
if allow_cors != "*":
if not isinstance(allow_cors, list):
allow_cors = [allow_cors]
if origin not in allow_cors:
return
response.headers.extend({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
'Cache-Control,Content-Type')
})
def make_form_dict(request):
import json
@ -145,6 +183,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) })
@ -160,6 +201,10 @@ def handle_exception(e):
http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False
if frappe.conf.get('developer_mode'):
# don't fail silently
print(frappe.get_traceback())
if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
# handle ajax responses first
# if the request is ajax, send back the trace or error message

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
@ -207,23 +207,44 @@ class LoginManager:
if frappe.session.user != "Guest":
clear_sessions(frappe.session.user, keep_current=True)
def authenticate(self, user=None, pwd=None):
def authenticate(self, user: str = None, pwd: str = None):
from frappe.core.doctype.user.user import User
if not (user and pwd):
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
if not (user and pwd):
self.fail(_('Incomplete login details'), user=user)
if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")):
user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
# Ignore password check if tmp_id is set, 2FA takes care of authentication.
validate_password = not bool(frappe.form_dict.get('tmp_id'))
user = User.find_by_credentials(user, pwd, validate_password=validate_password)
if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_user_name")):
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
if not user:
self.fail('Invalid login credentials')
self.check_if_enabled(user)
if not frappe.form_dict.get('tmp_id'):
self.user = self.check_password(user, pwd)
sys_settings = frappe.get_doc("System Settings")
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0)
tracker_kwargs = {}
if track_login_attempts:
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts
tracker = LoginAttemptTracker(user.name, **tracker_kwargs)
if track_login_attempts and not tracker.is_user_allowed():
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
.format(sys_settings.allow_login_after_fail), frappe.SecurityException)
if not user.is_authenticated:
tracker.add_failure_attempt()
self.fail('Invalid login credentials', user=user.name)
elif not (user.name == 'Administrator' or user.enabled):
tracker.add_failure_attempt()
self.fail('User disabled or missing', user=user.name)
else:
self.user = user
tracker.add_success_attempt()
self.user = user.name
def force_user_to_reset_password(self):
if not self.user:
@ -245,23 +266,12 @@ class LoginManager:
if last_pwd_reset_days > reset_pwd_after_days:
return True
def check_if_enabled(self, user):
"""raise exception if user not enabled"""
doc = frappe.get_doc("System Settings")
if cint(doc.allow_consecutive_login_attempts) > 0:
check_consecutive_login_attempts(user, doc)
if user=='Administrator': return
if not cint(frappe.db.get_value('User', user, 'enabled')):
self.fail('User disabled or missing', user=user)
def check_password(self, user, pwd):
"""check password"""
try:
# returns user in correct case
return check_password(user, pwd)
except frappe.AuthenticationError:
self.update_invalid_login(user)
self.fail('Incorrect password', user=user)
def fail(self, message, user=None):
@ -272,15 +282,6 @@ class LoginManager:
frappe.db.commit()
raise frappe.AuthenticationError
def update_invalid_login(self, user):
last_login_tried = get_last_tried_login_data(user)
failed_count = 0
if last_login_tried > get_datetime():
failed_count = get_login_failed_count(user)
frappe.cache().hset('login_failed_count', user, failed_count + 1)
def run_trigger(self, event='on_login'):
for method in frappe.get_hooks().get(event, []):
frappe.call(frappe.get_attr(method), login_manager=self)
@ -383,38 +384,6 @@ def clear_cookies():
frappe.session.sid = ""
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"])
def get_last_tried_login_data(user, get_last_login=False):
locked_account_time = frappe.cache().hget('locked_account_time', user)
if get_last_login and locked_account_time:
return locked_account_time
last_login_tried = frappe.cache().hget('last_login_tried', user)
if not last_login_tried or last_login_tried < get_datetime():
last_login_tried = get_datetime() + datetime.timedelta(seconds=60)
frappe.cache().hset('last_login_tried', user, last_login_tried)
return last_login_tried
def get_login_failed_count(user):
return cint(frappe.cache().hget('login_failed_count', user)) or 0
def check_consecutive_login_attempts(user, doc):
login_failed_count = get_login_failed_count(user)
last_login_tried = (get_last_tried_login_data(user, True)
+ datetime.timedelta(seconds=doc.allow_login_after_fail))
if login_failed_count >= cint(doc.allow_consecutive_login_attempts):
locked_account_time = frappe.cache().hget('locked_account_time', user)
if not locked_account_time:
frappe.cache().hset('locked_account_time', user, get_datetime())
if last_login_tried > get_datetime():
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
.format(doc.allow_login_after_fail), frappe.SecurityException)
else:
delete_login_failed_cache(user)
def validate_ip_address(user):
"""check if IP Address is valid"""
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
@ -436,3 +405,87 @@ def validate_ip_address(user):
return
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
class LoginAttemptTracker(object):
"""Track login attemts of a user.
Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in.
"""
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60):
""" Initialize the tracker.
:param user_name: Name of the loggedin user
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
:param lock_interval: Locking interval incase of maximum failed attempts
"""
self.user_name = user_name
self.lock_interval = datetime.timedelta(seconds=lock_interval)
self.max_failed_logins = max_consecutive_login_attempts
@property
def login_failed_count(self):
return frappe.cache().hget('login_failed_count', self.user_name)
@login_failed_count.setter
def login_failed_count(self, count):
frappe.cache().hset('login_failed_count', self.user_name, count)
@login_failed_count.deleter
def login_failed_count(self):
frappe.cache().hdel('login_failed_count', self.user_name)
@property
def login_failed_time(self):
"""First failed login attempt time within lock interval.
For every user we track only First failed login attempt time within lock interval of time.
"""
return frappe.cache().hget('login_failed_time', self.user_name)
@login_failed_time.setter
def login_failed_time(self, timestamp):
frappe.cache().hset('login_failed_time', self.user_name, timestamp)
@login_failed_time.deleter
def login_failed_time(self):
frappe.cache().hdel('login_failed_time', self.user_name)
def add_failure_attempt(self):
""" Log user failure attempts into the system.
Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count.
"""
login_failed_time = self.login_failed_time
login_failed_count = self.login_failed_count # Consecutive login failure count
current_time = get_datetime()
if not (login_failed_time and login_failed_count):
login_failed_time, login_failed_count = current_time, 0
if login_failed_time + self.lock_interval > current_time:
login_failed_count += 1
else:
login_failed_time, login_failed_count = current_time, 1
self.login_failed_time = login_failed_time
self.login_failed_count = login_failed_count
def add_success_attempt(self):
"""Reset login failures.
"""
del self.login_failed_count
del self.login_failed_time
def is_user_allowed(self) -> bool:
"""Is user allowed to login
User is not allowed to login if login failures are greater than threshold within in lock interval from first login failure.
"""
login_failed_time = self.login_failed_time
login_failed_count = self.login_failed_count or 0
current_time = get_datetime()
if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins:
return False
return True

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

@ -3,14 +3,80 @@
frappe.ui.form.on('Assignment Rule', {
refresh: function(frm) {
frm.trigger('setup_assignment_days_buttons');
frm.trigger('set_options');
// refresh description
frm.events.rule(frm);
},
setup: function(frm) {
frm.set_query("document_type", () => {
return {
filters: {
name: ["!=", "ToDo"]
}
};
});
},
document_type: function(frm) {
frm.trigger('set_options');
},
setup_assignment_days_buttons: function(frm) {
const labels = ['Weekends', 'Weekdays', 'All Days'];
let get_days = (label) => {
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const weekends = ['Saturday', 'Sunday'];
return {
'All Days': weekdays.concat(weekends),
'Weekdays': weekdays,
'Weekends': weekends,
}[label];
};
let set_days = (e) => {
frm.clear_table('assignment_days');
const label = $(e.currentTarget).text();
get_days(label).forEach((day) =>
frm.add_child('assignment_days', { day: day })
);
frm.refresh_field('assignment_days');
};
labels.forEach(label =>
frm.fields_dict['assignment_days'].grid.add_custom_button(
label,
set_days,
'top'
)
);
},
rule: function(frm) {
if (frm.doc.rule === 'Round Robin') {
frm.get_field('rule').set_description(__('Assign one by one, in sequence'));
} else {
frm.get_field('rule').set_description(__('Assign to the one who has the least assignments'));
const description_map = {
'Round Robin': __('Assign one by one, in sequence'),
'Load Balancing': __('Assign to the one who has the least assignments'),
'Based on Field': __('Assign to the user set in this field'),
};
frm.get_field('rule').set_description(description_map[frm.doc.rule]);
},
set_options(frm) {
const doctype = frm.doc.document_type;
frm.set_fields_as_options(
'field',
doctype,
(df) => ['Dynamic Link', 'Data'].includes(df.fieldtype)
|| (df.fieldtype == 'Link' && df.options == 'User'),
[{ label: 'Owner', value: 'owner' }]
);
if (doctype) {
frm.set_fields_as_options(
'due_date_based_on',
doctype,
(df) => ['Date', 'Datetime'].includes(df.fieldtype)
).then(options => frm.set_df_property('due_date_based_on', 'hidden', !options.length));
}
}
},
});

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2019-02-28 17:12:18.815830",
@ -8,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"document_type",
"due_date_based_on",
"priority",
"disabled",
"column_break_4",
@ -22,6 +24,7 @@
"assignment_days",
"assign_to_users_section",
"rule",
"field",
"users",
"last_user"
],
@ -91,15 +94,16 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Rule",
"options": "Round Robin\nLoad Balancing",
"options": "Round Robin\nLoad Balancing\nBased on Field",
"reqd": 1
},
{
"depends_on": "eval: doc.rule !== 'Based on Field'",
"fieldname": "users",
"fieldtype": "Table MultiSelect",
"label": "Users",
"options": "Assignment Rule User",
"reqd": 1
"mandatory_depends_on": "eval: doc.rule !== 'Based on Field'",
"options": "Assignment Rule User"
},
{
"fieldname": "last_user",
@ -129,9 +133,25 @@
"label": "Assignment Days",
"options": "Assignment Rule Day",
"reqd": 1
},
{
"depends_on": "document_type",
"description": "Value from this field will be set as the due date in the ToDo",
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On"
},
{
"depends_on": "eval: doc.rule == 'Based on Field'",
"fieldname": "field",
"fieldtype": "Select",
"label": "Field",
"mandatory_depends_on": "eval: doc.rule == 'Based on Field'"
}
],
"modified": "2019-09-25 14:52:12.214514",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-20 14:47:20.662954",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",

View file

@ -18,15 +18,17 @@ class AssignmentRule(Document):
if not len(set(assignment_days)) == len(assignment_days):
repeated_days = get_repeated(assignment_days)
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
if self.document_type == 'ToDo':
frappe.throw(_('Assignment Rule is not allowed on {0} document type').format(frappe.bold("ToDo")))
def on_update(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_update(self):
clear_assignment_rule_cache(self)
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def after_rename(self, old, new, merge):
clear_assignment_rule_cache(self)
def on_trash(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_trash(self):
clear_assignment_rule_cache(self)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -38,26 +40,30 @@ class AssignmentRule(Document):
def apply_assign(self, doc):
if self.safe_eval('assign_condition', doc):
self.do_assignment(doc)
return True
return self.do_assignment(doc)
def do_assignment(self, doc):
# clear existing assignment, to reassign
assign_to.clear(doc.get('doctype'), doc.get('name'))
user = self.get_user()
user = self.get_user(doc)
assign_to.add(dict(
assign_to = [user],
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name,
notify = True
))
if user:
assign_to.add(dict(
assign_to = [user],
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name,
notify = True,
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
))
# set for reference in round robin
self.db_set('last_user', user)
# set for reference in round robin
self.db_set('last_user', user)
return True
return False
def clear_assignment(self, doc):
'''Clear assignments'''
@ -69,7 +75,7 @@ class AssignmentRule(Document):
if self.safe_eval('close_condition', doc):
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name'))
def get_user(self):
def get_user(self, doc):
'''
Get the next user for assignment
'''
@ -77,6 +83,8 @@ class AssignmentRule(Document):
return self.get_user_round_robin()
elif self.rule == 'Load Balancing':
return self.get_user_load_balancing()
elif self.rule == 'Based on Field':
return self.get_user_based_on_field(doc)
def get_user_round_robin(self):
'''
@ -113,6 +121,11 @@ class AssignmentRule(Document):
# pick the first user
return sorted_counts[0].get('user')
def get_user_based_on_field(self, doc):
val = doc.get(self.field)
if frappe.db.exists('User', val):
return val
def safe_eval(self, fieldname, doc):
try:
if self.get(fieldname):
@ -188,7 +201,7 @@ def apply(doc, method=None, doctype=None, name=None):
# multiple auto assigns
for d in assignment_rules:
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.get('name')))
assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name')))
if not assignment_rule_docs:
return
@ -237,6 +250,40 @@ def apply(doc, method=None, doctype=None, name=None):
break
assignment_rule.close_assignments(doc)
def update_due_date(doc, state=None):
# called from hook
if (frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
return
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict(
document_type = doc.doctype,
disabled = 0,
due_date_based_on = ['is', 'set']
))
for rule in assignment_rules:
rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name'))
due_date_field = rule_doc.due_date_based_on
if doc.meta.has_field(due_date_field) and \
doc.has_value_changed(due_date_field) and rule.get('name'):
assignment_todos = frappe.get_all('ToDo', {
'assignment_rule': rule.get('name'),
'status': 'Open',
'reference_type': doc.doctype,
'reference_name': doc.name
})
for todo in assignment_todos:
todo_doc = frappe.get_doc('ToDo', todo.name)
todo_doc.date = doc.get(due_date_field)
todo_doc.flags.updater_reference = {
'doctype': 'Assignment Rule',
'docname': rule.get('name'),
'label': _('via Assignment Rule')
}
todo_doc.save(ignore_permissions=True)
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
@ -250,3 +297,7 @@ def get_repeated(values):
if value not in diff:
diff.append(str(value))
return " ".join(diff)
def clear_assignment_rule_cache(rule):
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)

View file

@ -20,6 +20,7 @@ class TestAutoAssign(unittest.TestCase):
dict(day = 'Friday'),
dict(day = 'Saturday'),
]
self.days = days
self.assignment_rule = get_assignment_rule([days, days])
clear_assignments()
@ -87,6 +88,30 @@ class TestAutoAssign(unittest.TestCase):
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
def test_based_on_field(self):
self.assignment_rule.rule = 'Based on Field'
self.assignment_rule.field = 'owner'
self.assignment_rule.save()
frappe.set_user('test1@example.com')
note = make_note(dict(public=1))
# check if auto assigned to doc owner, test1@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test1@example.com')
frappe.set_user('test2@example.com')
note = make_note(dict(public=1))
# check if auto assigned to doc owner, test2@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test2@example.com')
frappe.set_user('Administrator')
def test_assign_condition(self):
# check condition
@ -180,6 +205,55 @@ class TestAutoAssign(unittest.TestCase):
status = 'Open'
), 'owner'), ['test3@example.com'])
def test_assignment_rule_condition(self):
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
# Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
create_custom_field('Note', df)
assignment_rule = frappe.get_doc(dict(
name = 'Assignment with Due Date',
doctype = 'Assignment Rule',
document_type = 'Note',
assign_condition = 'public == 0',
due_date_based_on = 'expiry_date',
assignment_days = self.days,
users = [
dict(user = 'test@example.com'),
]
)).insert()
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
note1 = make_note({'expiry_date': expiry_date})
note2 = make_note({'expiry_date': expiry_date})
note1_todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note1.name,
status = 'Open'
))[0]
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name)
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)
# due date should be updated if the reference doc's date is updated.
note1.expiry_date = frappe.utils.add_days(expiry_date, 2)
note1.save()
note1_todo_doc.reload()
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)
# saving one note's expiry should not update other note todo's due date
note2_todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note2.name,
status = 'Open'
), fields=['name', 'date'])[0]
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
assignment_rule.delete()
def clear_assignments():
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
@ -237,4 +311,4 @@ def make_note(values=None):
note.insert()
return note
return note

View file

@ -1,76 +1,34 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"allow_read": 1,
"creation": "2019-02-27 11:41:46.602400",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"index_web_pages_for_search": 1,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-27 17:16:41.399261",
"links": [],
"modified": "2020-09-29 20:12:14.456785",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule User",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

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]));
}
@ -44,6 +44,22 @@ frappe.ui.form.on('Auto Repeat', {
// auto repeat schedule
frappe.auto_repeat.render_schedule(frm);
frm.trigger('toggle_submit_on_creation');
},
reference_doctype: function(frm) {
frm.trigger('toggle_submit_on_creation');
},
toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
if (frm.doc.reference_doctype) {
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
}
},
template: function(frm) {
@ -86,15 +102,13 @@ frappe.ui.form.on('Auto Repeat', {
frappe.auto_repeat.render_schedule = function(frm) {
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
frm.call("get_auto_repeat_schedule").then(r => {
frm.dashboard.wrapper.empty();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {
schedule_details : r.message || []
})
schedule_details: r.message || []
}),
__('Auto Repeat Schedule')
);
frm.dashboard.show();
});

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:AUT-AR-{#####}",
@ -12,6 +13,7 @@
"section_break_3",
"reference_doctype",
"reference_document",
"submit_on_creation",
"column_break_5",
"start_date",
"end_date",
@ -21,6 +23,8 @@
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
"section_break_16",
"repeat_on_days",
"notification",
"notify_by_email",
"recipients",
@ -186,9 +190,28 @@
"fieldname": "repeat_on_last_day",
"fieldtype": "Check",
"label": "Repeat on Last Day of the Month"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "repeat_on_days",
"fieldtype": "Table",
"label": "Repeat on Days",
"options": "Auto Repeat Day"
},
{
"default": "0",
"fieldname": "submit_on_creation",
"fieldtype": "Check",
"label": "Submit on Creation"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "section_break_16",
"fieldtype": "Section Break"
}
],
"modified": "2019-07-17 11:30:51.412317",
"links": [],
"modified": "2021-01-12 09:24:49.719611",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
@ -13,16 +14,21 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
class AutoRepeat(Document):
def validate(self):
self.update_status()
self.validate_reference_doctype()
self.validate_submit_on_creation()
self.validate_dates()
self.validate_email_id()
self.validate_auto_repeat_days()
self.set_dates()
self.update_auto_repeat_id()
self.unlink_if_applicable()
@ -48,7 +54,7 @@ class AutoRepeat(Document):
if self.disabled:
self.next_schedule_date = None
else:
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
@ -60,6 +66,11 @@ class AutoRepeat(Document):
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
def validate_submit_on_creation(self):
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
frappe.bold('Submit on Creation')))
def validate_dates(self):
if frappe.flags.in_patch:
return
@ -82,6 +93,12 @@ class AutoRepeat(Document):
else:
frappe.throw(_("'Recipients' not specified"))
def validate_auto_repeat_days(self):
auto_repeat_days = self.get_auto_repeat_days()
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
repeated_days = get_repeated(auto_repeat_days)
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
def update_auto_repeat_id(self):
#check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
@ -107,7 +124,7 @@ class AutoRepeat(Document):
end_date = getdate(self.end_date)
if not self.end_date:
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
next_date = self.get_next_schedule_date(schedule_date=start_date)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
@ -116,8 +133,7 @@ class AutoRepeat(Document):
schedule_details.append(row)
if self.end_date:
next_date = get_next_schedule_date(
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
while (getdate(next_date) < getdate(end_date)):
row = {
@ -126,8 +142,7 @@ class AutoRepeat(Document):
"next_scheduled_date" : next_date
}
schedule_details.append(row)
next_date = get_next_schedule_date(
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
return schedule_details
@ -150,6 +165,9 @@ class AutoRepeat(Document):
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
if self.submit_on_creation:
new_doc.submit()
return new_doc
def update_doc(self, new_doc, reference_doc):
@ -160,7 +178,7 @@ class AutoRepeat(Document):
if new_doc.meta.get_field('auto_repeat'):
new_doc.set('auto_repeat', self.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname))
@ -202,6 +220,75 @@ class AutoRepeat(Document):
new_doc.set('from_date', from_date)
new_doc.set('to_date', to_date)
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
"""
Returns the next schedule date for auto repeat after a recurring document has been created.
Adds required offset to the schedule_date param and returns the next schedule date.
:param schedule_date: The date when the last recurring document was created.
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
"""
if month_map.get(self.frequency):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and self.repeat_on_last_day:
day_count = 31
next_date = get_next_date(self.start_date, month_count, day_count)
elif month_count and self.repeat_on_day:
day_count = self.repeat_on_day
next_date = get_next_date(self.start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(self.start_date, month_count)
else:
days = self.get_days(schedule_date)
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(self.frequency, 0)
next_date = get_next_date(self.start_date, month_count, day_count)
else:
days = self.get_days(next_date)
next_date = add_days(next_date, days)
return next_date
def get_days(self, schedule_date):
if self.frequency == "Weekly":
days = self.get_offset_for_weekly_frequency(schedule_date)
else:
# daily frequency
days = 1
return days
def get_offset_for_weekly_frequency(self, schedule_date):
# if weekdays are not set, offset is 7 from current schedule date
if not self.repeat_on_days:
return 7
repeat_on_days = self.get_auto_repeat_days()
current_schedule_day = getdate(schedule_date).weekday()
weekdays = list(week_map.keys())
# if repeats on more than 1 day or
# start date's weekday is not in repeat days, then get next weekday
# else offset is 7
if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
weekday = get_next_weekday(current_schedule_day, repeat_on_days)
next_weekday_number = week_map.get(weekday, 0)
# offset for upcoming weekday
return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
return 7
def get_auto_repeat_days(self):
return [d.day for d in self.get('repeat_on_days', [])]
def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation"""
subject = self.subject or ''
@ -243,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)
@ -282,42 +364,24 @@ class AutoRepeat(Document):
)
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
if month_map.get(frequency):
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and repeat_on_last_day:
day_count = 31
next_date = get_next_date(start_date, month_count, day_count)
elif month_count and repeat_on_day:
day_count = repeat_on_day
next_date = get_next_date(start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(frequency)
next_date = get_next_date(start_date, month_count, day_count)
elif days:
next_date = add_days(next_date, days)
return next_date
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
def get_next_weekday(current_schedule_day, weekdays):
days = list(week_map.keys())
if current_schedule_day > 0:
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
else:
days = days[(current_schedule_day + 1):]
for entry in days:
if entry in weekdays:
return entry
#called through hooks
def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
@ -328,6 +392,7 @@ def make_auto_repeat_entry():
data = get_auto_repeat_entries(date)
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
@ -337,10 +402,11 @@ def create_repeated_entries(data):
if schedule_date == current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
def get_auto_repeat_entries(date=None):
if not date:
date = getdate(today())
@ -349,6 +415,7 @@ def get_auto_repeat_entries(date=None):
['status', '=', 'Active']
])
#called through hooks
def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
@ -358,6 +425,7 @@ def set_auto_repeat_as_completed():
doc.status = 'Completed'
doc.save()
@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
if not start_date:

View file

@ -7,10 +7,9 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields():
df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase):
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.get_doc('ToDo', new_todo)
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat_with_weekdays(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
weekdays = list(week_map.keys())
current_weekday = getdate().weekday()
days = [
{'day': weekdays[current_weekday]},
{'day': weekdays[(current_weekday + 2) % 7]}
]
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
doc.reload()
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2))
def test_monthly_auto_repeat(self):
start_date = today()
end_date = add_months(start_date, 12)
@ -111,6 +156,25 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
self.assertEqual(getdate(doc.next_schedule_date), current_date)
def test_submit_on_creation(self):
doctype = 'Test Submittable DocType'
create_submittable_doctype(doctype)
current_date = getdate()
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
submittable_doc.submit()
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
start_date=add_days(current_date, -1), submit_on_creation=1)
data = get_auto_repeat_entries(current_date)
create_repeated_entries(data)
docnames = frappe.db.get_all(doc.reference_doctype,
filters={'auto_repeat': doc.name},
fields=['docstatus'],
limit=1
)
self.assertEquals(docnames[0].docstatus, 1)
def make_auto_repeat(**args):
args = frappe._dict(args)
@ -118,13 +182,46 @@ def make_auto_repeat(**args):
'doctype': 'Auto Repeat',
'reference_doctype': args.reference_doctype or 'ToDo',
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
'submit_on_creation': args.submit_on_creation or 0,
'frequency': args.frequency or 'Daily',
'start_date': args.start_date or add_days(today(), -1),
'end_date': args.end_date or "",
'notify_by_email': args.notify or 0,
'recipients': args.recipients or "",
'subject': args.subject or "",
'message': args.message or ""
'message': args.message or "",
'repeat_on_days': args.days or []
}).insert(ignore_permissions=True)
return doc
def create_submittable_doctype(doctype):
if frappe.db.exists('DocType', doctype):
return
else:
doc = frappe.get_doc({
'doctype': 'DocType',
'__newname': doctype,
'module': 'Custom',
'custom': 1,
'is_submittable': 1,
'fields': [{
'fieldname': 'test',
'label': 'Test',
'fieldtype': 'Data'
}],
'permissions': [{
'role': 'System Manager',
'read': 1,
'write': 1,
'create': 1,
'delete': 1,
'submit': 1,
'cancel': 1,
'amend': 1
}]
}).insert()
doc.allow_auto_repeat = 1
doc.save()

View file

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

View file

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

View file

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

@ -15,7 +15,7 @@ import frappe
from frappe.utils.minify import JavascriptMinify
import click
from requests import get
import psutil
from six import iteritems, text_type
from six.moves.urllib.parse import urlparse
@ -26,6 +26,8 @@ sites_path = os.path.abspath(os.getcwd())
def download_file(url, prefix):
from requests import get
filename = urlparse(url).path.split("/")[-1]
local_filename = os.path.join(prefix, filename)
with get(url, stream=True, allow_redirects=True) as r:
@ -40,6 +42,7 @@ def build_missing_files():
# check which files dont exist yet from the build.json and tell build.js to build only those!
missing_assets = []
current_asset_files = []
frappe_build = os.path.join("..", "apps", "frappe", "frappe", "public", "build.json")
for type in ["css", "js"]:
current_asset_files.extend(
@ -49,7 +52,7 @@ def build_missing_files():
]
)
with open(os.path.join(sites_path, "assets", "frappe", "build.json")) as f:
with open(frappe_build) as f:
all_asset_files = json.load(f).keys()
for asset in all_asset_files:
@ -104,20 +107,28 @@ def download_frappe_assets(verbose=True):
if frappe_head:
try:
url = get_assets_link(frappe_head)
click.secho("Retreiving assets...", fg="yellow")
click.secho("Retrieving assets...", fg="yellow")
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
assets_archive = download_file(url, prefix)
print("\n{0} Downloaded Frappe assets from {1}".format(green(''), url))
if assets_archive:
import tarfile
directories_created = set()
click.secho("\nExtracting assets...\n", fg="yellow")
with tarfile.open(assets_archive) as tar:
for file in tar:
if not file.isdir():
dest = "." + file.name.replace("./frappe-bench/sites", "")
asset_directory = os.path.dirname(dest)
show = dest.replace("./assets/", "")
if asset_directory not in directories_created:
if not os.path.exists(asset_directory):
os.makedirs(asset_directory, exist_ok=True)
directories_created.add(asset_directory)
tar.makefile(file, dest)
print("{0} Restored {1}".format(green(''), show))
@ -216,7 +227,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False,
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe.commands.popen(command, cwd=frappe_app_path)
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
def watch(no_compress):
@ -228,13 +239,32 @@ def watch(no_compress):
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen("{pacman} run watch".format(pacman=pacman), cwd=frappe_app_path)
frappe.commands.popen("{pacman} run watch".format(pacman=pacman),
cwd=frappe_app_path, env=get_node_env())
def check_yarn():
if not find_executable("yarn"):
print("Please install yarn using below command and try again.\nnpm install -g yarn")
def get_node_env():
node_env = {
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
}
return node_env
def get_safe_max_old_space_size():
safe_max_old_space_size = 0
try:
total_memory = psutil.virtual_memory().total / (1024 * 1024)
# reference for the safe limit assumption
# https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes
# set minimum value 1GB
safe_max_old_space_size = max(1024, int(total_memory * 0.75))
except Exception:
pass
return safe_max_old_space_size
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.

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,19 +67,18 @@ 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()
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)
@ -101,8 +100,14 @@ 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)
def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache()

View file

@ -1,27 +1,21 @@
from __future__ import unicode_literals
# imports - standard imports
import json
from collections.abc import MutableMapping, MutableSequence, Sequence
# imports - third-party imports
import requests
# imports - compatibility imports
import six
# imports - standard imports
from collections import Sequence, MutableSequence, Mapping, MutableMapping
if six.PY2:
from urlparse import urlparse # PY2
else:
from urllib.parse import urlparse # PY3
import json
from urllib.parse import urlparse
# imports - module imports
from frappe.model.document import Document
from frappe.exceptions import DuplicateEntryError
from frappe import _dict
import frappe
from frappe.exceptions import DuplicateEntryError
from frappe.model.document import Document
session = frappe.session
def get_user_doc(user = None):
if isinstance(user, Document):
return user
@ -38,12 +32,12 @@ def squashify(what):
return what
def safe_json_loads(*args):
results = [ ]
results = []
for arg in args:
try:
arg = json.loads(arg)
except Exception as e:
except Exception:
pass
results.append(arg)
@ -81,7 +75,7 @@ def dictify(arg):
for i, a in enumerate(arg):
arg[i] = dictify(a)
elif isinstance(arg, MutableMapping):
arg = _dict(arg)
arg = frappe._dict(arg)
return arg
@ -113,4 +107,4 @@ def get_emojis():
emojis = resp.json()
redis.hset('frappe_emojis', 'emojis', emojis)
return dictify(emojis)
return dictify(emojis)

View file

@ -11,6 +11,7 @@ import frappe.utils
import subprocess # nosec
from functools import wraps
from six import StringIO
from os import environ
click.disable_unicode_literals_warning = True
@ -53,16 +54,20 @@ def get_site(context, raise_err=True):
return None
def popen(command, *args, **kwargs):
output = kwargs.get('output', True)
cwd = kwargs.get('cwd')
shell = kwargs.get('shell', True)
output = kwargs.get('output', True)
cwd = kwargs.get('cwd')
shell = kwargs.get('shell', True)
raise_err = kwargs.get('raise_err')
env = kwargs.get('env')
if env:
env = dict(environ, **env)
proc = subprocess.Popen(command,
stdout = None if output else subprocess.PIPE,
stderr = None if output else subprocess.PIPE,
shell = shell,
cwd = cwd
stdout=None if output else subprocess.PIPE,
stderr=None if output else subprocess.PIPE,
shell=shell,
cwd=cwd,
env=env
)
return_ = proc.wait()

View file

@ -1,10 +1,5 @@
# imports - standard imports
import atexit
import compileall
import hashlib
import os
import re
import shutil
import sys
# imports - third party imports
@ -13,10 +8,7 @@ import click
# imports - module imports
import frappe
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.exceptions import SiteNotSpecifiedError
from frappe.installer import update_site_config
from frappe.utils import get_site_path, touch_file
@click.command('new-site')
@ -38,6 +30,8 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None):
"Create a new site"
from frappe.installer import _new_site
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
@ -49,54 +43,6 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None,
db_port=None, new_site=False):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
print('Site {0} already exists'.format(site))
sys.exit(1)
if no_mariadb_socket and not db_type == "mariadb":
print('--no-mariadb-socket requires db_type to be set to mariadb.')
sys.exit(1)
if not db_name:
db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16]
from frappe.installer import install_db, make_site_dirs
from frappe.installer import install_app as _install_app
import frappe.utils.scheduler
frappe.init(site=site)
try:
# enable scheduler post install?
enable_scheduler = _is_scheduler_enabled()
except Exception:
enable_scheduler = False
make_site_dirs()
installing = touch_file(get_site_path('locks', 'installing.lock'))
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
os.remove(installing)
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
@click.command('restore')
@click.argument('sql-file-path')
@ -107,36 +53,46 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
@click.option('--install-app', multiple=True, help='Install app after installation')
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done')
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
@pass_context
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
"Restore site database from an sql file"
from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade
from frappe.installer import (
_new_site,
extract_sql_from_archive,
extract_files,
is_downgrade,
is_partial,
validate_database_sql
)
force = context.force or force
decompressed_file_name = extract_sql_from_archive(sql_file_path)
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
if not os.path.exists(sql_file_path):
base_path = '..'
sql_file_path = os.path.join(base_path, sql_file_path)
if not os.path.exists(sql_file_path):
print('Invalid path {0}'.format(sql_file_path[3:]))
sys.exit(1)
elif sql_file_path.startswith(os.sep):
base_path = os.sep
else:
base_path = '.'
# check if partial backup
if is_partial(decompressed_file_name):
click.secho(
"Partial Backup file detected. You cannot use a partial file to restore a Frappe Site.",
fg="red"
)
click.secho(
"Use `bench partial-restore` to restore a partial backup to an existing site.",
fg="yellow"
)
sys.exit(1)
if sql_file_path.endswith('sql.gz'):
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
else:
decompressed_file_name = sql_file_path
# check if valid SQL file
validate_database_sql(decompressed_file_name, _raise=not force)
site = get_site(context)
frappe.init(site=site)
# dont allow downgrading to older versions of frappe without force
if not force and is_downgrade(decompressed_file_name, verbose=True):
warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
warn_message = (
"This is not recommended and may lead to unexpected behaviour. "
"Do you want to continue anyway?"
)
click.confirm(warn_message, abort=True)
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
@ -146,22 +102,39 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
with_public_files = os.path.join(base_path, with_public_files)
public = extract_tar_files(site, with_public_files, 'public')
public = extract_files(site, with_public_files)
os.remove(public)
if with_private_files:
with_private_files = os.path.join(base_path, with_private_files)
private = extract_tar_files(site, with_private_files, 'private')
private = extract_files(site, with_private_files)
os.remove(private)
# Removing temporarily created file
if decompressed_file_name != sql_file_path:
os.remove(decompressed_file_name)
success_message = "Site {0} has been restored{1}".format(site, " with files" if (with_public_files or with_private_files) else "")
success_message = "Site {0} has been restored{1}".format(
site,
" with files" if (with_public_files or with_private_files) else ""
)
click.secho(success_message, fg="green")
@click.command('partial-restore')
@click.argument('sql-file-path')
@click.option("--verbose", "-v", is_flag=True)
@pass_context
def partial_restore(context, sql_file_path, verbose):
from frappe.installer import partial_restore
verbose = context.verbose or verbose
site = get_site(context)
frappe.init(site=site)
frappe.connect(site=site)
partial_restore(sql_file_path, verbose)
frappe.destroy()
@click.command('reinstall')
@click.option('--admin-password', help='Administrator Password for reinstalled site')
@click.option('--mariadb-root-username', help='Root username for MariaDB')
@ -174,6 +147,8 @@ def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_
_reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose)
def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False):
from frappe.installer import _new_site
if not yes:
click.confirm('This will wipe your database. Are you sure you want to reinstall?', abort=True)
try:
@ -226,15 +201,51 @@ def install_app(context, apps):
sys.exit(exit_code)
@click.command('list-apps')
@click.command("list-apps")
@pass_context
def list_apps(context):
"List apps in site"
site = get_site(context)
frappe.init(site=site)
frappe.connect()
print("\n".join(frappe.get_installed_apps()))
frappe.destroy()
def fix_whitespaces(text):
if site == context.sites[-1]:
text = text.rstrip()
if len(context.sites) == 1:
text = text.lstrip()
return text
for site in context.sites:
frappe.init(site=site)
frappe.connect()
site_title = (
click.style(f"{site}", fg="green") if len(context.sites) > 1 else ""
)
apps = frappe.get_single("Installed Applications").installed_applications
if apps:
name_len, ver_len = [
max([len(x.get(y)) for x in apps])
for y in ["app_name", "app_version"]
]
template = "{{0:{0}}} {{1:{1}}} {{2}}".format(name_len, ver_len)
installed_applications = [
template.format(app.app_name, app.app_version, app.git_branch)
for app in apps
]
applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
else:
applications_summary = "\n".join(frappe.get_installed_apps())
summary = f"{site_title}\n{applications_summary}\n"
summary = fix_whitespaces(summary)
if applications_summary and summary:
print(summary)
frappe.destroy()
@click.command('add-system-manager')
@click.argument('email')
@ -269,13 +280,13 @@ def disable_user(context, email):
user.save(ignore_permissions=True)
frappe.db.commit()
@click.command('migrate')
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
@pass_context
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
import re
from frappe.migrate import migrate
for site in context.sites:
@ -293,9 +304,6 @@ def migrate(context, skip_failing=False, skip_search_index=False):
if not context.sites:
raise SiteNotSpecifiedError
print("Compiling Python files...")
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
@click.command('migrate-to')
@click.argument('frappe_provider')
@pass_context
@ -312,15 +320,16 @@ def migrate_to(context, frappe_provider):
@click.command('run-patch')
@click.argument('module')
@click.option('--force', is_flag=True)
@pass_context
def run_patch(context, module):
def run_patch(context, module, force):
"Run a particular patch"
import frappe.modules.patch_handler
for site in context.sites:
frappe.init(site=site)
try:
frappe.connect()
frappe.modules.patch_handler.run_single(module, force=context.force)
frappe.modules.patch_handler.run_single(module, force=force or context.force)
finally:
frappe.destroy()
if not context.sites:
@ -385,35 +394,54 @@ def use(site, sites_path='.'):
@click.command('backup')
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
@click.option('--verbose', default=False, is_flag=True)
@click.option('--include', '--only', '-i', default="", type=str, help="Specify the DocTypes to backup seperated by commas")
@click.option('--exclude', '-e', default="", type=str, help="Specify the DocTypes to not backup seperated by commas")
@click.option('--backup-path', default=None, help="Set path for saving all the files in this operation")
@click.option('--backup-path-db', default=None, help="Set path for saving database file")
@click.option('--backup-path-files', default=None, help="Set path for saving public file")
@click.option('--backup-path-private-files', default=None, help="Set path for saving private file")
@click.option('--backup-path-conf', default=None, help="Set path for saving config file")
@click.option('--ignore-backup-conf', default=False, is_flag=True, help="Ignore excludes/includes set in config")
@click.option('--verbose', default=False, is_flag=True, help="Add verbosity")
@click.option('--compress', default=False, is_flag=True, help="Compress private and public files")
@pass_context
def backup(context, with_files=False, backup_path_db=None, backup_path_files=None,
backup_path_private_files=None, quiet=False, verbose=False):
def backup(context, with_files=False, backup_path=None, backup_path_db=None, backup_path_files=None,
backup_path_private_files=None, backup_path_conf=None, ignore_backup_conf=False, verbose=False,
compress=False, include="", exclude=""):
"Backup"
from frappe.utils.backups import scheduled_backup
verbose = verbose or context.verbose
exit_code = 0
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose)
except Exception as e:
odb = scheduled_backup(
ignore_files=not with_files,
backup_path=backup_path,
backup_path_db=backup_path_db,
backup_path_files=backup_path_files,
backup_path_private_files=backup_path_private_files,
backup_path_conf=backup_path_conf,
ignore_conf=ignore_backup_conf,
include_doctypes=include,
exclude_doctypes=exclude,
compress=compress,
verbose=verbose,
force=True
)
except Exception:
click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
if verbose:
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
print(frappe.get_traceback())
exit_code = 1
continue
if verbose:
from frappe.utils import now
summary_title = "Backup Summary at {0}".format(now())
print(summary_title + "\n" + "-" * len(summary_title))
print("Database backup:", odb.backup_path_db)
if with_files:
print("Public files: ", odb.backup_path_files)
print("Private files: ", odb.backup_path_private_files)
odb.print_summary()
click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@ -482,13 +510,14 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
if force:
pass
else:
click.echo("="*80)
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n"))
click.echo("Fix the issue and try again.")
click.echo(
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
)
messages = [
"=" * 80,
"Error: The operation has stopped because backup of {0}'s database failed.".format(site),
"Reason: {0}\n".format(str(err)),
"Fix the issue and try again.",
"Hint: Use 'bench drop-site {0} --force' to force the removal of {0}".format(site)
]
click.echo("\n".join(messages))
sys.exit(1)
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
@ -618,8 +647,10 @@ def browse(context, site):
@click.command('start-recording')
@pass_context
def start_recording(context):
import frappe.recorder
for site in context.sites:
frappe.init(site=site)
frappe.set_user("Administrator")
frappe.recorder.start()
if not context.sites:
raise SiteNotSpecifiedError
@ -628,8 +659,10 @@ def start_recording(context):
@click.command('stop-recording')
@pass_context
def stop_recording(context):
import frappe.recorder
for site in context.sites:
frappe.init(site=site)
frappe.set_user("Administrator")
frappe.recorder.stop()
if not context.sites:
raise SiteNotSpecifiedError
@ -700,5 +733,6 @@ commands = [
stop_recording,
add_to_hosts,
start_ngrok,
build_search_index
build_search_index,
partial_restore
]

View file

@ -460,11 +460,21 @@ def console(context):
frappe.init(site=site)
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
import IPython
all_apps = frappe.get_installed_apps()
failed_to_import = []
for app in all_apps:
locals()[app] = __import__(app)
try:
locals()[app] = __import__(app)
except ModuleNotFoundError:
failed_to_import.append(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
if failed_to_import:
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
IPython.embed(display_banner="", header="", colors="neutral")
@ -554,10 +564,25 @@ def run_ui_tests(context, app, headless=False):
site_env = 'CYPRESS_baseUrl={}'.format(site_url)
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
os.chdir(app_base_path)
node_bin = subprocess.getoutput("npm bin")
cypress_path = "{0}/cypress".format(node_bin)
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)) \
or not subprocess.getoutput("npm view cypress version").startswith("6."):
# install cypress
click.secho("Installing Cypress...", fg="yellow")
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'
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
command = '{site_env} {password_env} {cypress} {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
click.secho("Running Cypress...", fg="yellow")
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)

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,66 +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")
},
{
"type": "doctype",
"label": _("Package"),
"name": "Package",
"description": _("Import and Export Packages.")
}
]
}
]

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,122 +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"),
},
]
},
{
"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,190 +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": "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

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:32",
@ -24,7 +25,6 @@
"is_shipping_address",
"disabled",
"linked_with",
"is_your_company_address",
"links"
],
"fields": [
@ -75,7 +75,7 @@
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
"label": "State/Province"
},
{
"fieldname": "country",
@ -138,12 +138,6 @@
"label": "Reference",
"options": "fa fa-pushpin"
},
{
"default": "0",
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"label": "Is Your Company Address"
},
{
"fieldname": "links",
"fieldtype": "Table",
@ -153,7 +147,8 @@
],
"icon": "fa fa-map-marker",
"idx": 5,
"modified": "2019-09-08 11:41:04.145589",
"links": [],
"modified": "2020-10-21 16:14:37.284830",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Address",

View file

@ -39,14 +39,13 @@ class Address(Document):
def validate(self):
self.link_address()
self.validate_reference()
self.validate_preferred_address()
set_link_title(self)
deduplicate_dynamic_links(self)
def link_address(self):
"""Link address based on owner"""
if not self.links and not self.is_your_company_address:
if not self.links:
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
if contact_name:
contact = frappe.get_cached_doc('Contact', contact_name)
@ -56,12 +55,6 @@ class Address(Document):
return False
def validate_reference(self):
if self.is_your_company_address:
if not [row for row in self.links if row.link_doctype == "Company"]:
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table below."),
title =_("Company not Linked"))
def validate_preferred_address(self):
preferred_fields = ['is_primary_address', 'is_shipping_address']
@ -204,25 +197,6 @@ def get_address_templates(address):
else:
return result
@frappe.whitelist()
def get_shipping_address(company, address = None):
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
["Address", "is_your_company_address", "=", 1]
]
fields = ["*"]
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
if address:
address_as_dict = address[0]
name, address_template = get_address_templates(address_as_dict)
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
def get_company_address(company):
ret = frappe._dict()
ret.company_address = get_default_address('Company', company)

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

@ -40,7 +40,11 @@ def add_authentication_log(subject, user, operation="Login", status="Success"):
"operation": operation,
}).insert(ignore_permissions=True, ignore_links=True)
def clear_authentication_logs():
"""clear 100 day old authentication logs"""
def clear_activity_logs(days=None):
"""clear 90 day old authentication logs or configured in log settings"""
if not days:
days = 90
frappe.db.sql("""delete from `tabActivity Log` where \
creation< (NOW() - INTERVAL '100' DAY)""")
creation< (NOW() - INTERVAL '{0}' DAY)""".format(days))

View file

@ -77,6 +77,10 @@ class TestActivityLog(unittest.TestCase):
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager)
# REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts
# before raising security exception, remove below line when that is fixed.
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.SecurityException, LoginManager)
time.sleep(5)
self.assertRaises(frappe.AuthenticationError, LoginManager)

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)
@ -150,7 +165,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
try:
# use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
(json.dumps(_comments[-50:]), reference_name))
(json.dumps(_comments[-100:]), reference_name))
except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):

View file

@ -99,8 +99,7 @@ frappe.ui.form.on("Communication", {
}
},
show_relink_dialog: function(frm){
var lib = "frappe.email";
show_relink_dialog: function(frm) {
var d = new frappe.ui.Dialog ({
title: __("Relink Communication"),
fields: [{
@ -138,8 +137,10 @@ frappe.ui.form.on("Communication", {
}
});
},
function () {
frappe.show_alert('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()
@ -260,10 +259,8 @@ class Communication(Document):
# Timeline Links
def set_timeline_links(self):
contacts = []
if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \
frappe.flags.in_test:
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled)
for contact_name in contacts:
self.add_link('Contact', contact_name)
@ -342,7 +339,7 @@ def get_permission_query_conditions_for_communication(user):
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))
def get_contacts(email_strings):
def get_contacts(email_strings, auto_create_contact=False):
email_addrs = []
for email_string in email_strings:
@ -357,7 +354,7 @@ def get_contacts(email_strings):
email = get_email_without_link(email)
contact_name = get_contact_name(email)
if not contact_name and email:
if not contact_name and email and auto_create_contact:
email_parts = email.split("@")
first_name = frappe.unscrub(email_parts[0])

View file

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

View file

@ -1,2 +1,2 @@
Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number
Test 26 ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7
Title,Description,Number,another_number,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number
Test 26,test description,1,2,"",child title,child description,child title,14-08-2019,4,child title again,22-09-2020,5,7

Can't render this file because it contains an unexpected character in line 2 and column 56.

View file

@ -616,7 +616,9 @@ class Row:
id_field = get_id_field(doctype)
id_value = doc.get(id_field.fieldname)
if id_value and frappe.db.exists(doctype, id_value):
doc = frappe.get_doc(doctype, id_value)
existing_doc = frappe.get_doc(doctype, id_value)
existing_doc.update(doc)
doc = existing_doc
else:
# for table rows being inserted in update
# create a new doc with defaults set
@ -749,7 +751,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
"message": _("{0} is a mandatory field asdadsf").format(id_field.label),
"message": _("{0} is a mandatory field").format(id_field.label),
}
)
return

View file

@ -5,12 +5,14 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.core.doctype.data_import.importer import Importer
from frappe.utils import getdate, format_duration
doctype_name = 'DocType for Import'
class TestImporter(unittest.TestCase):
def setUp(self):
@classmethod
def setUpClass(cls):
create_doctype_if_not_exists(doctype_name)
def test_data_import_from_file(self):
@ -71,19 +73,28 @@ class TestImporter(unittest.TestCase):
self.assertEqual(warnings[2]['message'], "<b>Title</b> is a mandatory field")
def test_data_import_update(self):
if not frappe.db.exists(doctype_name, 'Test 26'):
frappe.get_doc(
doctype=doctype_name,
title='Test 26'
).insert()
existing_doc = frappe.get_doc(
doctype=doctype_name,
title=frappe.generate_hash(doctype_name, 8),
table_field_1=[{'child_title': 'child title to update'}]
)
existing_doc.save()
frappe.db.commit()
import_file = get_import_file('sample_import_file_for_update')
data_import = self.get_importer(doctype_name, import_file, update=True)
data_import.start_import()
i = Importer(data_import.reference_doctype, data_import=data_import)
updated_doc = frappe.get_doc(doctype_name, 'Test 26')
# update child table id in template date
i.import_file.raw_data[1][4] = existing_doc.table_field_1[0].name
i.import_file.raw_data[1][0] = existing_doc.name
i.import_file.parse_data_from_template()
i.import_data()
updated_doc = frappe.get_doc(doctype_name, existing_doc.name)
self.assertEqual(updated_doc.description, 'test description')
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title')
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name)
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description')
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again')

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

@ -13,6 +13,7 @@
"fieldname",
"precision",
"length",
"non_negative",
"hide_days",
"hide_seconds",
"reqd",
@ -473,13 +474,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-28 11:28:21.252853",
"modified": "2020-10-29 06:09:26.454990",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

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

View file

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

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