Merge branch 'develop' of https://github.com/frappe/frappe into version-13-pre-release
This commit is contained in:
commit
288df4fe0d
801 changed files with 33247 additions and 41847 deletions
|
|
@ -1,2 +0,0 @@
|
|||
exclude_paths:
|
||||
- '**.sql'
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
.github/helper/translation.py
vendored
10
.github/helper/translation.py
vendored
|
|
@ -2,7 +2,7 @@ import re
|
|||
import sys
|
||||
|
||||
errors_encounter = 0
|
||||
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
|
||||
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
|
||||
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
|
||||
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
|
||||
f_string_pattern = re.compile(r"_\(f[\"']")
|
||||
|
|
@ -28,7 +28,7 @@ for _file in files_to_scan:
|
|||
has_f_string = f_string_pattern.search(line)
|
||||
if has_f_string:
|
||||
errors_encounter += 1
|
||||
print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}')
|
||||
print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}')
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
|
@ -36,7 +36,7 @@ for _file in files_to_scan:
|
|||
match = pattern.search(line)
|
||||
error_found = False
|
||||
|
||||
if not match and line.endswith(',\n'):
|
||||
if not match and line.endswith((',\n', '[\n')):
|
||||
# concat remaining text to validate multiline pattern
|
||||
line = "".join(file_lines[line_number - 1:])
|
||||
line = line[start_matches.start() + 1:]
|
||||
|
|
@ -44,11 +44,11 @@ for _file in files_to_scan:
|
|||
|
||||
if not match:
|
||||
error_found = True
|
||||
print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}')
|
||||
print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}')
|
||||
|
||||
if not error_found and not words_pattern.search(line):
|
||||
error_found = True
|
||||
print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}')
|
||||
print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}')
|
||||
|
||||
if error_found:
|
||||
errors_encounter += 1
|
||||
|
|
|
|||
22
.github/workflows/semgrep.yml
vendored
Normal file
22
.github/workflows/semgrep.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Run semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
if [ -f .semgrep.yml ]; then semgrep --config=.semgrep.yml --quiet --error $files; fi
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ pull_request_rules:
|
|||
- name: Automatic squash on CI success and review
|
||||
conditions:
|
||||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=dont-merge
|
||||
|
|
|
|||
29
.semgrep.yml
Normal file
29
.semgrep.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#Reference: https://semgrep.dev/docs/writing-rules/rule-syntax/
|
||||
|
||||
rules:
|
||||
- id: eval
|
||||
patterns:
|
||||
- pattern-not: eval("...")
|
||||
- pattern: eval(...)
|
||||
message: |
|
||||
Detected the use of eval(). eval() can be dangerous if used to evaluate
|
||||
dynamic content. Avoid it or use safe_eval().
|
||||
languages:
|
||||
- python
|
||||
severity: ERROR
|
||||
|
||||
# translations
|
||||
- id: frappe-translation-syntax-python
|
||||
pattern-either:
|
||||
- pattern: _(f"...") # f-strings not allowed
|
||||
- pattern: _("..." + "...") # concatenation not allowed
|
||||
- pattern: _("") # empty string is meaningless
|
||||
- pattern: _("..." % ...) # Only positional formatters are allowed.
|
||||
- pattern: _("...".format(...)) # format should not be used before translating
|
||||
- pattern: _("...") + ... + _("...") # don't split strings
|
||||
message: |
|
||||
Incorrect use of translation function detected.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages:
|
||||
- python
|
||||
severity: ERROR
|
||||
9
.stylelintrc
Normal file
9
.stylelintrc
Normal 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
|
||||
}
|
||||
}
|
||||
11
.travis.yml
11
.travis.yml
|
|
@ -7,7 +7,7 @@ addons:
|
|||
- test_site_producer
|
||||
mariadb: 10.3
|
||||
postgresql: 9.5
|
||||
chrome: stable
|
||||
firefox: latest
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
|
@ -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'";
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
14
CODEOWNERS
14
CODEOWNERS
|
|
@ -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/ @nextchamp-saqib
|
||||
patches/ @sahil28297
|
||||
website/ @prssanna
|
||||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
email/ @saurabh6790
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -3,5 +3,9 @@
|
|||
"projectId": "92odwv",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000
|
||||
"pageLoadTimeout": 15000,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ context('Awesome Bar', () => {
|
|||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('.navbar-header .navbar-home').click();
|
||||
cy.get('.navbar .navbar-home').click();
|
||||
});
|
||||
|
||||
it('navigates to doctype list', () => {
|
||||
|
|
@ -14,16 +14,16 @@ context('Awesome Bar', () => {
|
|||
cy.get('#navbar-search + ul').should('be.visible');
|
||||
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
|
||||
|
||||
cy.get('h1').should('contain', 'To Do');
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.location('hash').should('eq', '#List/ToDo/List');
|
||||
cy.location('pathname').should('eq', '/app/todo');
|
||||
});
|
||||
|
||||
it('find text in doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('h1').should('contain', 'To Do');
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.get('[data-original-title="Name"] > .input-with-feedback')
|
||||
.should('have.value', '%test%');
|
||||
|
|
@ -33,7 +33,7 @@ context('Awesome Bar', () => {
|
|||
cy.get('#navbar-search')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post 1');
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
|
||||
});
|
||||
|
||||
it('calculates math expressions', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
context('Control Barcode', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_barcode() {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
context('Control Link', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for link'
|
||||
|
|
@ -29,8 +29,7 @@ context('Control Link', () => {
|
|||
it('should set the valid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
|
||||
cy.wait('@search_link');
|
||||
|
|
@ -50,8 +49,7 @@ context('Control Link', () => {
|
|||
it('should unset invalid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.type('invalid value', { delay: 100 })
|
||||
|
|
@ -63,9 +61,8 @@ context('Control Link', () => {
|
|||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.get('.frappe-control[data-fieldname=link] input').as('input');
|
||||
|
|
@ -77,7 +74,7 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=link] .link-btn')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`);
|
||||
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
36
cypress/integration/control_select.js
Normal file
36
cypress/integration/control_select.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
context('Depends On', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
|
||||
name: 'Child Test Depends On',
|
||||
|
|
@ -64,7 +64,7 @@ context('Depends On', () => {
|
|||
cy.fill_field('test_field', 'Some Value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
|
||||
cy.get('body').click();
|
||||
cy.hide_dialog();
|
||||
cy.fill_field('test_field', 'Random value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
|
||||
|
|
@ -92,7 +92,7 @@ context('Depends On', () => {
|
|||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click();
|
||||
cy.get('@row1-form_in_grid').find('.grid-collapse-row').click();
|
||||
|
||||
// set the table to read-only
|
||||
cy.fill_field('test_field', 'Some Other Value');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
context('FileUploader', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
function open_upload_dialog() {
|
||||
|
|
@ -19,44 +19,36 @@ context('FileUploader', () => {
|
|||
it('should accept dropped files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.fixture('example.json').then(fileContent => {
|
||||
cy.get_open_dialog().find('.file-upload-area').upload({
|
||||
fileContent,
|
||||
fileName: 'example.json',
|
||||
mimeType: 'application/json'
|
||||
}, {
|
||||
subjectType: 'drag-n-drop',
|
||||
force: true
|
||||
});
|
||||
cy.get_open_dialog().find('.file-info').should('contain', 'example.json');
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.wait('@upload_file').its('status').should('be', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
cy.get_open_dialog().find('.file-upload-area').attachFile('example.json', {
|
||||
subjectType: 'drag-n-drop',
|
||||
});
|
||||
|
||||
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-modal-primary').click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should accept uploaded files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('a:contains("uploaded file")').click();
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click();
|
||||
cy.get('.file-filter').type('example.json');
|
||||
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', '/private/files/example.json');
|
||||
.should('have.property', 'file_name', 'example.json');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should accept web links', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('a:contains("web link")').click();
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', 'https://github.com');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,18 +3,27 @@ context('Recorder', () => {
|
|||
cy.login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/app/recorder');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
// reset recorder
|
||||
return frappe.xcall("frappe.recorder.stop").then(() => {
|
||||
return frappe.xcall("frappe.recorder.delete");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.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 +33,38 @@ context('Recorder', () => {
|
|||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.visit('/desk#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.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.visit('/desk#recorder');
|
||||
it.only('Recorder View Request', () => {
|
||||
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.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.url().should('include', '/recorder/request');
|
||||
cy.get('form').should('contain', '/api/method/frappe');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,46 +4,44 @@ context('Relative Timeframe', () => {
|
|||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('sets relative timespan filter for last week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
cy.get('.list-row-container').should('contain', 'this is second todo');
|
||||
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.get('.remove-filter.btn').click();
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
it('sets relative timespan filter for next week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
cy.get('.list-row').should('contain', 'this is first todo');
|
||||
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.get('.remove-filter.btn').click();
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,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 => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -244,14 +244,14 @@ Cypress.Commands.add('awesomebar', text => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('new_form', doctype => {
|
||||
let route = `Form/${doctype}/New ${doctype} 1`;
|
||||
cy.visit(`/desk#${route}`);
|
||||
cy.get('body').should('have.attr', 'data-route', route);
|
||||
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
|
||||
cy.visit(`/app/${dt_in_route}/new`);
|
||||
cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`);
|
||||
cy.get('body').should('have.attr', 'data-ajax-state', 'complete');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('go_to_list', doctype => {
|
||||
cy.visit(`/desk#List/${doctype}/List`);
|
||||
cy.visit(`/app/list/${doctype}/list`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_cache', () => {
|
||||
|
|
@ -275,9 +275,8 @@ Cypress.Commands.add('get_open_dialog', () => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('hide_dialog', () => {
|
||||
cy.get_open_dialog()
|
||||
.find('.btn-modal-close')
|
||||
.click();
|
||||
cy.wait(300);
|
||||
cy.get_open_dialog().find('.btn-modal-close').click();
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
|
|
@ -307,4 +306,21 @@ Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
|||
return res.body.data;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('add_filter', () => {
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.wait(300);
|
||||
cy.get('.filter-popover').should('exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_filters', () => {
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.wait(300);
|
||||
cy.get('.filter-popover').should('exist');
|
||||
cy.get('.filter-popover').find('.clear-filters').click();
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.window().its('cur_list').then(cur_list => {
|
||||
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ import './commands';
|
|||
// require('./commands')
|
||||
|
||||
Cypress.Cookies.defaults({
|
||||
whitelist: 'sid'
|
||||
preserve: 'sid'
|
||||
});
|
||||
|
|
@ -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,11 +17,15 @@ 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
|
||||
# Local application imports
|
||||
from .exceptions import *
|
||||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||
from .utils.lazy_loader import lazy_import
|
||||
|
||||
# Lazy imports
|
||||
faker = lazy_import('faker')
|
||||
|
||||
|
||||
# Harmless for Python 3
|
||||
# For Python 2 set default encoding to utf-8
|
||||
|
|
@ -191,17 +201,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
|
||||
|
|
@ -463,11 +476,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**.
|
||||
|
||||
|
||||
|
|
@ -493,6 +506,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:
|
||||
|
|
@ -510,12 +524,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 = []
|
||||
|
|
@ -965,10 +979,6 @@ def get_installed_apps(sort=False, frappe_last=False):
|
|||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value('all_apps', get_all_apps)
|
||||
|
||||
#cache bench apps
|
||||
if not cache().get_value('all_apps'):
|
||||
cache().set_value('all_apps', local.all_apps)
|
||||
|
||||
installed = json.loads(db.get_global("installed_apps") or "[]")
|
||||
|
||||
if sort:
|
||||
|
|
@ -1193,10 +1203,10 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
|
|||
ps.validate_fieldtype_change()
|
||||
ps.insert()
|
||||
|
||||
def import_doc(path, ignore_links=False, ignore_insert=False, insert=False):
|
||||
def import_doc(path):
|
||||
"""Import a file using Data Import."""
|
||||
from frappe.core.doctype.data_import.data_import import import_doc
|
||||
import_doc(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert)
|
||||
import_doc(path)
|
||||
|
||||
def copy_doc(doc, ignore_no_copy=True):
|
||||
""" No_copy fields also get copied."""
|
||||
|
|
@ -1633,7 +1643,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,
|
||||
|
|
@ -1746,12 +1756,12 @@ def parse_json(val):
|
|||
|
||||
def mock(type, size=1, locale='en'):
|
||||
results = []
|
||||
faker = Faker(locale)
|
||||
if not type in dir(faker):
|
||||
fake = faker.Faker(locale)
|
||||
if type not in dir(fake):
|
||||
raise ValueError('Not a valid mock type.')
|
||||
else:
|
||||
for i in range(size):
|
||||
data = getattr(faker, type)()
|
||||
data = getattr(fake, type)()
|
||||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ 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)
|
||||
|
||||
|
|
@ -181,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) })
|
||||
|
|
|
|||
177
frappe/auth.py
177
frappe/auth.py
|
|
@ -173,7 +173,7 @@ class LoginManager:
|
|||
frappe.local.cookie_manager.set_cookie("system_user", "yes")
|
||||
if not resume:
|
||||
frappe.local.response['message'] = 'Logged In'
|
||||
frappe.local.response["home_page"] = "/desk"
|
||||
frappe.local.response["home_page"] = "/app"
|
||||
|
||||
if not resume:
|
||||
frappe.response["full_name"] = self.full_name
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,6 +9,16 @@ frappe.ui.form.on('Assignment Rule', {
|
|||
frm.events.rule(frm);
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
frm.set_query("document_type", () => {
|
||||
return {
|
||||
filters: {
|
||||
name: ["!=", "ToDo"]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
document_type: function(frm) {
|
||||
frm.trigger('set_options');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ 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):
|
||||
clear_assignment_rule_cache(self)
|
||||
|
|
@ -298,4 +300,4 @@ def get_repeated(values):
|
|||
|
||||
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)
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
refresh: function(frm) {
|
||||
// auto repeat message
|
||||
if (frm.is_new()) {
|
||||
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
|
||||
let customize_form_link = `<a href="/app/customize form">${__('Customize Form')}</a>`;
|
||||
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
|
||||
}
|
||||
|
||||
|
|
@ -106,8 +106,9 @@ frappe.auto_repeat.render_schedule = function(frm) {
|
|||
frm.dashboard.wrapper.empty();
|
||||
frm.dashboard.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
schedule_details : r.message || []
|
||||
})
|
||||
schedule_details: r.message || []
|
||||
}),
|
||||
__('Auto Repeat Schedule')
|
||||
);
|
||||
frm.dashboard.show();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ from frappe.model.document import Document
|
|||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
|
||||
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
|
||||
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
|
||||
|
||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
|
||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
|
|
@ -328,13 +330,8 @@ class AutoRepeat(Document):
|
|||
|
||||
def fetch_linked_contacts(self):
|
||||
if self.reference_doctype and self.reference_document:
|
||||
res = frappe.db.get_all('Contact',
|
||||
fields=['email_id'],
|
||||
filters=[
|
||||
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
|
||||
['Dynamic Link', 'link_name', '=', self.reference_document]
|
||||
])
|
||||
|
||||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
email_ids = list(set([d.email_id for d in res]))
|
||||
if not email_ids:
|
||||
frappe.msgprint(_('No contacts linked to document'), alert=True)
|
||||
|
|
|
|||
229
frappe/automation/workspace/tools/tools.json
Normal file
229
frappe/automation/workspace/tools/tools.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -225,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):
|
||||
|
|
@ -237,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.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ common_default_keys = ["__default", "__global"]
|
|||
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
|
||||
'milestone_tracker_map', 'event_consumer_document_type_map')
|
||||
|
||||
global_cache_keys = ("app_hooks", "installed_apps",
|
||||
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
|
||||
"app_modules", "module_app", "system_settings",
|
||||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
|
|
@ -67,10 +67,6 @@ def clear_defaults_cache(user=None):
|
|||
elif frappe.flags.in_install!="frappe":
|
||||
frappe.cache().delete_key("defaults")
|
||||
|
||||
def clear_document_cache():
|
||||
frappe.local.document_cache = {}
|
||||
frappe.cache().delete_key("document_cache")
|
||||
|
||||
def clear_doctype_cache(doctype=None):
|
||||
clear_controller_cache(doctype)
|
||||
cache = frappe.cache()
|
||||
|
|
@ -78,9 +74,11 @@ def clear_doctype_cache(doctype=None):
|
|||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
|
||||
del frappe.local.meta_cache[doctype]
|
||||
|
||||
for key in ('is_table', 'doctype_modules'):
|
||||
for key in ('is_table', 'doctype_modules', 'document_cache'):
|
||||
cache.delete_value(key)
|
||||
|
||||
frappe.local.document_cache = {}
|
||||
|
||||
def clear_single(dt):
|
||||
for name in doctype_cache_keys:
|
||||
cache.hdel(name, dt)
|
||||
|
|
@ -102,15 +100,12 @@ def clear_doctype_cache(doctype=None):
|
|||
for name in doctype_cache_keys:
|
||||
cache.delete_value(name)
|
||||
|
||||
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
|
||||
clear_document_cache()
|
||||
|
||||
def clear_controller_cache(doctype=None):
|
||||
if not doctype:
|
||||
del frappe.controllers
|
||||
frappe.controllers = {}
|
||||
return
|
||||
|
||||
|
||||
for site_controllers in frappe.controllers.values():
|
||||
site_controllers.pop(doctype, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import click
|
|||
import frappe
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.installer import _new_site
|
||||
|
||||
|
||||
@click.command('new-site')
|
||||
|
|
@ -31,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,
|
||||
|
|
@ -57,6 +58,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
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 (
|
||||
_new_site,
|
||||
extract_sql_from_archive,
|
||||
extract_files,
|
||||
is_downgrade,
|
||||
|
|
@ -145,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:
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ def import_doc(context, path, force=False):
|
|||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
import_doc(path, overwrite=context.force)
|
||||
import_doc(path)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
|
|
@ -571,13 +571,14 @@ def run_ui_tests(context, app, headless=False):
|
|||
plugin_path = "{0}/cypress-file-upload".format(node_bin)
|
||||
|
||||
# check if cypress in path...if not, install it.
|
||||
if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)):
|
||||
if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \
|
||||
or not subprocess.getoutput("npm view cypress version").startswith("6."):
|
||||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen("yarn add cypress@3 cypress-file-upload@^3.1 --no-lockfile")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else '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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Form Customization"),
|
||||
"icon": "fa fa-glass",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Customize Form",
|
||||
"description": _("Change field properties (hide, readonly, permission etc.)")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Custom Field",
|
||||
"description": _("Add fields to forms.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Custom Script",
|
||||
"description": _("Add custom javascript to forms.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "DocType",
|
||||
"description": _("Add custom forms.")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Dashboards"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dashboard",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dashboard Chart",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dashboard Chart Source",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Other"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"label": _("Custom Translations"),
|
||||
"name": "Translation",
|
||||
"description": _("Add your own translations")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
},
|
||||
]
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
source_link = "https://github.com/frappe/frappe_io"
|
||||
docs_base_url = "/docs"
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Payments"),
|
||||
"icon": "fa fa-star",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Braintree Settings",
|
||||
"description": _("Braintree payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "PayPal Settings",
|
||||
"description": _("PayPal payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Razorpay Settings",
|
||||
"description": _("Razorpay Payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Stripe Settings",
|
||||
"description": _("Stripe payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Paytm Settings",
|
||||
"description": _("Paytm payment gateway settings"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Backup"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dropbox Settings",
|
||||
"description": _("Dropbox backup settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "S3 Backup Settings",
|
||||
"description": _("S3 Backup Settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Drive",
|
||||
"description": _("Google Drive Backup."),
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Authentication"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Social Login Key",
|
||||
"description": _("Enter keys to enable login via Facebook, Google, GitHub."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "LDAP Settings",
|
||||
"description": _("Ldap settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "OAuth Client",
|
||||
"description": _("Register OAuth Client App"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "OAuth Provider Settings",
|
||||
"description": _("Settings for OAuth Provider"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Connected App",
|
||||
"description": _("Connect to any OAuth Provider"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Webhook"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Webhook",
|
||||
"description": _("Webhooks calling API requests into web apps"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Slack Webhook URL",
|
||||
"description": _("Slack Webhooks for internal integration"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Google Services"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Settings",
|
||||
"description": _("Google API Settings."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Contacts",
|
||||
"description": _("Google Contacts Integration."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Calendar",
|
||||
"description": _("Google Calendar Integration."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Drive",
|
||||
"description": _("Google Drive Integration."),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.moduleview import add_setup_section
|
||||
|
||||
def get_data():
|
||||
data = [
|
||||
{
|
||||
"label": _("Core"),
|
||||
"icon": "fa fa-wrench",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "System Settings",
|
||||
"label": _("System Settings"),
|
||||
"description": _("Language, Date and Time settings"),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Global Defaults",
|
||||
"label": _("Global Defaults"),
|
||||
"description": _("Company, Fiscal Year and Currency defaults"),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Log Settings",
|
||||
"description": _("Log cleanup and notification configuration")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Log",
|
||||
"description": _("Log of error on automated events (scheduler).")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Snapshot",
|
||||
"description": _("Log of error during requests.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Domain Settings",
|
||||
"label": _("Domain Settings"),
|
||||
"description": _("Enable / Disable Domains"),
|
||||
"hide_count": True
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Data"),
|
||||
"icon": "fa fa-th",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Data Import",
|
||||
"label": _("Import Data"),
|
||||
"icon": "octicon octicon-cloud-upload",
|
||||
"description": _("Import Data from CSV / Excel files.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Data Export",
|
||||
"label": _("Export Data"),
|
||||
"icon": "octicon octicon-cloud-upload",
|
||||
"description": _("Export Data in CSV / Excel format.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Naming Series",
|
||||
"description": _("Set numbering series for transactions."),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Rename Tool",
|
||||
"label": _("Bulk Rename"),
|
||||
"description": _("Rename many items by uploading a .csv file."),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Bulk Update",
|
||||
"label": _("Bulk Update"),
|
||||
"description": _("Update many values at one time."),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"name": "backups",
|
||||
"label": _("Download Backups"),
|
||||
"description": _("List of backups available for download"),
|
||||
"icon": "fa fa-download"
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Deleted Document",
|
||||
"label": _("Deleted Documents"),
|
||||
"description": _("Restore or permanently delete a document.")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Email / Notifications"),
|
||||
"icon": "fa fa-envelope",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Account",
|
||||
"description": _("Add / Manage Email Accounts.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Domain",
|
||||
"description": _("Add / Manage Email Domains.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Notification",
|
||||
"description": _("Setup Notifications based on various criteria.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Template",
|
||||
"description": _("Email Templates for common queries.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Auto Email Report",
|
||||
"description": _("Setup Reports to be emailed at regular intervals"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Newsletter",
|
||||
"description": _("Create and manage newsletter")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"route": "Form/Notification Settings/{}".format(frappe.session.user),
|
||||
"name": "Notification Settings",
|
||||
"description": _("Configure notifications for mentions, assignments, energy points and more.")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Printing"),
|
||||
"icon": "fa fa-print",
|
||||
"items": [
|
||||
{
|
||||
"type": "page",
|
||||
"label": _("Print Format Builder"),
|
||||
"name": "print-format-builder",
|
||||
"description": _("Drag and Drop tool to build and customize Print Formats.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Settings",
|
||||
"description": _("Set default format, page size, print style etc.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Format",
|
||||
"description": _("Customized HTML Templates for printing transactions.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Style",
|
||||
"description": _("Stylesheets for Print Formats")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Workflow"),
|
||||
"icon": "fa fa-random",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Workflow",
|
||||
"description": _("Define workflows for forms.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Workflow State",
|
||||
"description": _("States for workflow (e.g. Draft, Approved, Cancelled).")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Workflow Action",
|
||||
"description": _("Actions for workflow (e.g. Approve, Cancel).")
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe")
|
||||
return data
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
|
@ -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")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -97,11 +97,16 @@ class Contact(Document):
|
|||
if len([email.email_id for email in self.email_ids if email.is_primary]) > 1:
|
||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID")))
|
||||
|
||||
primary_email_exists = False
|
||||
for d in self.email_ids:
|
||||
if d.is_primary == 1:
|
||||
primary_email_exists = True
|
||||
self.email_id = d.email_id.strip()
|
||||
break
|
||||
|
||||
if not primary_email_exists:
|
||||
self.email_id = ""
|
||||
|
||||
def set_primary(self, fieldname):
|
||||
# Used to set primary mobile and phone no.
|
||||
if len(self.phone_nos) == 0:
|
||||
|
|
@ -115,11 +120,16 @@ class Contact(Document):
|
|||
if len(is_primary) > 1:
|
||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
|
||||
|
||||
primary_number_exists = False
|
||||
for d in self.phone_nos:
|
||||
if d.get(field_name) == 1:
|
||||
primary_number_exists = True
|
||||
setattr(self, fieldname, d.phone)
|
||||
break
|
||||
|
||||
if not primary_number_exists:
|
||||
setattr(self, fieldname, "")
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
'''Returns default contact for the given doctype, name'''
|
||||
out = frappe.db.sql('''select parent,
|
||||
|
|
@ -256,3 +266,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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class TestAddressesAndContacts(unittest.TestCase):
|
|||
create_linked_contact(links_list, d)
|
||||
report_data = get_data({"reference_doctype": "Test Custom Doctype"})
|
||||
for idx, link in enumerate(links_list):
|
||||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', None, 'test_contact@example.com', 1]
|
||||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1]
|
||||
self.assertListEqual(test_item, report_data[idx])
|
||||
|
||||
def tearDown(self):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from frappe.core.doctype.data_import.importer import Importer
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import.exporter import Exporter
|
||||
from frappe.core.doctype.data_import.importer import Importer
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.import_file import import_file_by_path
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.csvutils import validate_google_sheets_url
|
||||
from frappe import _
|
||||
|
||||
|
||||
class DataImport(Document):
|
||||
|
|
@ -173,15 +173,7 @@ def import_file(
|
|||
##############
|
||||
|
||||
|
||||
def import_doc(
|
||||
path,
|
||||
overwrite=False,
|
||||
ignore_links=False,
|
||||
ignore_insert=False,
|
||||
insert=False,
|
||||
submit=False,
|
||||
pre_process=None,
|
||||
):
|
||||
def import_doc(path, pre_process=None):
|
||||
if os.path.isdir(path):
|
||||
files = [os.path.join(path, f) for f in os.listdir(path)]
|
||||
else:
|
||||
|
|
@ -190,30 +182,21 @@ def import_doc(
|
|||
for f in files:
|
||||
if f.endswith(".json"):
|
||||
frappe.flags.mute_emails = True
|
||||
frappe.modules.import_file.import_file_by_path(
|
||||
f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True
|
||||
import_file_by_path(
|
||||
f,
|
||||
data_import=True,
|
||||
force=True,
|
||||
pre_process=pre_process,
|
||||
reset_permissions=True
|
||||
)
|
||||
frappe.flags.mute_emails = False
|
||||
frappe.db.commit()
|
||||
elif f.endswith(".csv"):
|
||||
import_file_by_path(
|
||||
f,
|
||||
ignore_links=ignore_links,
|
||||
overwrite=overwrite,
|
||||
submit=submit,
|
||||
pre_process=pre_process,
|
||||
)
|
||||
validate_csv_import_file(f)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def import_file_by_path(
|
||||
path,
|
||||
ignore_links=False,
|
||||
overwrite=False,
|
||||
submit=False,
|
||||
pre_process=None,
|
||||
no_email=True,
|
||||
):
|
||||
def validate_csv_import_file(path):
|
||||
if path.endswith(".csv"):
|
||||
print()
|
||||
print("This method is deprecated.")
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import typing
|
||||
|
||||
import frappe
|
||||
from frappe.model import (
|
||||
display_fieldtypes,
|
||||
no_value_fields,
|
||||
table_fields as table_fieldtypes,
|
||||
)
|
||||
from frappe.utils import flt, format_duration
|
||||
from frappe.utils import flt, format_duration, groupby_metric
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
from frappe.utils.xlsxutils import build_xlsx_response
|
||||
|
||||
|
|
@ -116,7 +118,6 @@ class Exporter:
|
|||
|
||||
def get_data_to_export(self):
|
||||
frappe.permissions.can_export(self.doctype, raise_exception=True)
|
||||
data_to_export = []
|
||||
|
||||
table_fields = [f for f in self.exportable_fields if f != self.doctype]
|
||||
data = self.get_data_as_docs()
|
||||
|
|
@ -128,14 +129,13 @@ class Exporter:
|
|||
if table_fields:
|
||||
# add child table data
|
||||
for f in table_fields:
|
||||
for i, child_row in enumerate(doc[f]):
|
||||
for i, child_row in enumerate(doc.get(f, [])):
|
||||
table_df = self.meta.get_field(f)
|
||||
child_doctype = table_df.options
|
||||
rows = self.add_data_row(child_doctype, child_row.parentfield, child_row, rows, i)
|
||||
|
||||
data_to_export += rows
|
||||
|
||||
return data_to_export
|
||||
for row in rows:
|
||||
yield row
|
||||
|
||||
def add_data_row(self, doctype, parentfield, doc, rows, row_idx):
|
||||
if len(rows) < row_idx + 1:
|
||||
|
|
@ -204,17 +204,13 @@ class Exporter:
|
|||
)
|
||||
child_data[key] = data
|
||||
|
||||
return self.merge_data(parent_data, child_data)
|
||||
|
||||
def merge_data(self, parent_data, child_data):
|
||||
# Group children data by parent name
|
||||
grouped_children_data = self.group_children_data_by_parent(child_data)
|
||||
for doc in parent_data:
|
||||
for table_field, table_rows in child_data.items():
|
||||
doc[table_field] = [row for row in table_rows if row.parent == doc.name]
|
||||
|
||||
return parent_data
|
||||
related_children_docs = grouped_children_data.get(doc.name, {})
|
||||
yield {**doc, **related_children_docs}
|
||||
|
||||
def add_header(self):
|
||||
|
||||
header = []
|
||||
for df in self.fields:
|
||||
is_parent = not df.is_child_table_field
|
||||
|
|
@ -261,3 +257,6 @@ class Exporter:
|
|||
|
||||
def build_xlsx_response(self):
|
||||
build_xlsx_response(self.get_csv_array_for_export(), self.doctype)
|
||||
|
||||
def group_children_data_by_parent(self, children_data: typing.Dict[str, list]):
|
||||
return groupby_metric(children_data, key='parent')
|
||||
|
|
|
|||
|
|
@ -472,32 +472,6 @@ class ImportFile:
|
|||
|
||||
doc = parent_doc
|
||||
|
||||
if self.import_type == INSERT:
|
||||
# check if there is atleast one row for mandatory table fields
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
mandatory_table_fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype in table_fieldtypes
|
||||
and df.reqd
|
||||
and len(doc.get(df.fieldname, [])) == 0
|
||||
]
|
||||
if len(mandatory_table_fields) == 1:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": first_row.row_number,
|
||||
"message": _("There should be atleast one row for {0} table").format(
|
||||
frappe.bold(mandatory_table_fields[0].label)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif mandatory_table_fields:
|
||||
fields_string = ", ".join([df.label for df in mandatory_table_fields])
|
||||
message = _("There should be atleast one row for the following tables: {0}").format(
|
||||
fields_string
|
||||
)
|
||||
self.warnings.append({"row": first_row.row_number, "message": message})
|
||||
|
||||
return doc, rows, data[len(rows) :]
|
||||
|
||||
def get_warnings(self):
|
||||
|
|
@ -626,7 +600,6 @@ class Row:
|
|||
new_doc.update(doc)
|
||||
doc = new_doc
|
||||
|
||||
self.check_mandatory_fields(doctype, doc, table_df)
|
||||
return doc
|
||||
|
||||
def validate_value(self, value, col):
|
||||
|
|
@ -727,66 +700,6 @@ class Row:
|
|||
pass
|
||||
return value
|
||||
|
||||
def check_mandatory_fields(self, doctype, doc, table_df=None):
|
||||
"""If import type is Insert:
|
||||
Check for mandatory fields (except table fields) in doc
|
||||
if import type is Update:
|
||||
Check for name field or autoname field in doc
|
||||
"""
|
||||
meta = frappe.get_meta(doctype)
|
||||
if self.import_type == UPDATE:
|
||||
if meta.istable:
|
||||
# when updating records with table rows,
|
||||
# there are two scenarios:
|
||||
# 1. if row 'name' is provided in the template
|
||||
# the table row will be updated
|
||||
# 2. if row 'name' is not provided
|
||||
# then a new row will be added
|
||||
# so we dont need to check for mandatory
|
||||
return
|
||||
|
||||
# for update, only ID (name) field is mandatory
|
||||
id_field = get_id_field(doctype)
|
||||
if doc.get(id_field.fieldname) in INVALID_VALUES:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field").format(id_field.label),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype not in table_fieldtypes
|
||||
and df.reqd
|
||||
and doc.get(df.fieldname) in INVALID_VALUES
|
||||
]
|
||||
|
||||
if not fields:
|
||||
return
|
||||
|
||||
def get_field_label(df):
|
||||
return "{0}{1}".format(df.label, " ({})".format(table_df.label) if table_df else "")
|
||||
|
||||
if len(fields) == 1:
|
||||
field_label = get_field_label(fields[0])
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field").format(frappe.bold(field_label)),
|
||||
}
|
||||
)
|
||||
else:
|
||||
fields_string = ", ".join([frappe.bold(get_field_label(df)) for df in fields])
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} are mandatory fields").format(fields_string),
|
||||
}
|
||||
)
|
||||
|
||||
def get_values(self, indexes):
|
||||
return [self.data[i] for i in indexes]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ doctype_name = 'DocType for Import'
|
|||
class TestImporter(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_doctype_if_not_exists(doctype_name)
|
||||
create_doctype_if_not_exists(doctype_name,)
|
||||
|
||||
def test_data_import_from_file(self):
|
||||
import_file = get_import_file('sample_import_file')
|
||||
|
|
@ -59,18 +59,18 @@ class TestImporter(unittest.TestCase):
|
|||
def test_data_import_without_mandatory_values(self):
|
||||
import_file = get_import_file('sample_import_file_without_mandatory')
|
||||
data_import = self.get_importer(doctype_name, import_file)
|
||||
frappe.local.message_log = []
|
||||
data_import.start_import()
|
||||
data_import.reload()
|
||||
warnings = frappe.parse_json(data_import.template_warnings)
|
||||
import_log = frappe.parse_json(data_import.import_log)
|
||||
self.assertEqual(import_log[0]['row_indexes'], [2,3])
|
||||
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
|
||||
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(warnings[0]['row'], 2)
|
||||
self.assertEqual(warnings[0]['message'], "<b>Child Title (Table Field 1)</b> is a mandatory field")
|
||||
|
||||
self.assertEqual(warnings[1]['row'], 3)
|
||||
self.assertEqual(warnings[1]['message'], "<b>Child Title (Table Field 1 Again)</b> is a mandatory field")
|
||||
|
||||
self.assertEqual(warnings[2]['row'], 4)
|
||||
self.assertEqual(warnings[2]['message'], "<b>Title</b> is a mandatory field")
|
||||
self.assertEqual(import_log[1]['row_indexes'], [4])
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['messages'][0])['message'], "Title is required")
|
||||
|
||||
def test_data_import_update(self):
|
||||
existing_doc = frappe.get_doc(
|
||||
|
|
@ -104,6 +104,8 @@ class TestImporter(unittest.TestCase):
|
|||
data_import.reference_doctype = doctype
|
||||
data_import.import_file = import_file.file_url
|
||||
data_import.insert()
|
||||
# Commit so that the first import failure does not rollback the Data Import insert.
|
||||
frappe.db.commit()
|
||||
|
||||
return data_import
|
||||
|
||||
|
|
|
|||
|
|
@ -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 + "%");
|
||||
|
|
|
|||
|
|
@ -2,20 +2,22 @@
|
|||
# Copyright (c) 2017, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, os
|
||||
from frappe import _
|
||||
import os
|
||||
|
||||
import frappe
|
||||
import frappe.modules.import_file
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import format_datetime
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import_legacy.importer import upload
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.import_file import import_file_by_path as _import_file_by_path
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.data import format_datetime
|
||||
|
||||
|
||||
class DataImportLegacy(Document):
|
||||
def autoname(self):
|
||||
if not self.name:
|
||||
self.name = "Import on " +format_datetime(self.creation)
|
||||
self.name = "Import on " + format_datetime(self.creation)
|
||||
|
||||
def validate(self):
|
||||
if not self.import_file:
|
||||
|
|
@ -33,6 +35,7 @@ class DataImportLegacy(Document):
|
|||
def get_importable_doctypes():
|
||||
return frappe.cache().hget("can_import", frappe.session.user)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_data(data_import):
|
||||
frappe.db.set_value("Data Import Legacy", data_import, "import_status", "In Progress", update_modified=False)
|
||||
|
|
@ -57,7 +60,7 @@ def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False,
|
|||
for f in files:
|
||||
if f.endswith(".json"):
|
||||
frappe.flags.mute_emails = True
|
||||
frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True)
|
||||
_import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True)
|
||||
frappe.flags.mute_emails = False
|
||||
frappe.db.commit()
|
||||
elif f.endswith(".csv"):
|
||||
|
|
@ -69,7 +72,7 @@ def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False,
|
|||
from frappe.utils.csvutils import read_csv_content
|
||||
print("Importing " + path)
|
||||
with open(path, "r") as infile:
|
||||
upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite,
|
||||
upload(rows=read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite,
|
||||
submit_after_import=submit, pre_process=pre_process)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"label": "Editable Grid"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.istable && !doc.issingle",
|
||||
"description": "Open a dialog with mandatory fields to create a new record quickly",
|
||||
"fieldname": "quick_entry",
|
||||
|
|
@ -427,7 +427,7 @@
|
|||
"label": "Allow Guest to View"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_web_view",
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "route",
|
||||
"fieldtype": "Data",
|
||||
"label": "Route"
|
||||
|
|
@ -555,7 +555,7 @@
|
|||
},
|
||||
{
|
||||
"group": "Customization",
|
||||
"link_doctype": "Custom Script",
|
||||
"link_doctype": "Client Script",
|
||||
"link_fieldname": "dt"
|
||||
},
|
||||
{
|
||||
|
|
@ -609,7 +609,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2020-09-24 13:13:58.227153",
|
||||
"modified": "2021-02-04 15:10:09.227205",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
@ -637,6 +637,7 @@
|
|||
"write": 1
|
||||
}
|
||||
],
|
||||
"route": "doctype",
|
||||
"search_fields": "module",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from frappe.database.schema import validate_column_name, validate_column_length
|
|||
from frappe.model.docfield import supports_translation
|
||||
from frappe.modules.import_file import get_file_path
|
||||
from frappe.model.meta import Meta
|
||||
|
||||
from frappe.desk.utils import validate_route_conflict
|
||||
|
||||
class InvalidFieldNameError(frappe.ValidationError): pass
|
||||
class UniqueFieldnameError(frappe.ValidationError): pass
|
||||
|
|
@ -63,6 +63,37 @@ class DocType(Document):
|
|||
|
||||
self.validate_name()
|
||||
|
||||
self.set_defaults_for_single_and_table()
|
||||
self.scrub_field_names()
|
||||
self.set_default_in_list_view()
|
||||
self.set_default_translatable()
|
||||
self.validate_series()
|
||||
self.validate_document_type()
|
||||
validate_fields(self)
|
||||
|
||||
if not self.istable:
|
||||
validate_permissions(self)
|
||||
|
||||
self.make_amendable()
|
||||
self.make_repeatable()
|
||||
self.validate_nestedset()
|
||||
self.validate_website()
|
||||
validate_links_table_fieldnames(self)
|
||||
|
||||
if not self.is_new():
|
||||
self.before_update = frappe.get_doc('DocType', self.name)
|
||||
self.setup_fields_to_fetch()
|
||||
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.default_print_format and not self.custom:
|
||||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
|
||||
|
||||
def after_insert(self):
|
||||
# clear user cache so that on the next reload this doctype is included in boot
|
||||
clear_user_cache(frappe.session.user)
|
||||
|
||||
def set_defaults_for_single_and_table(self):
|
||||
if self.issingle:
|
||||
self.allow_import = 0
|
||||
self.is_submittable = 0
|
||||
|
|
@ -72,44 +103,6 @@ class DocType(Document):
|
|||
self.allow_import = 0
|
||||
self.permissions = []
|
||||
|
||||
self.scrub_field_names()
|
||||
self.set_default_in_list_view()
|
||||
self.set_default_translatable()
|
||||
self.validate_series()
|
||||
self.validate_document_type()
|
||||
validate_fields(self)
|
||||
|
||||
if self.istable:
|
||||
# no permission records for child table
|
||||
self.permissions = []
|
||||
else:
|
||||
validate_permissions(self)
|
||||
|
||||
self.make_amendable()
|
||||
self.make_repeatable()
|
||||
self.validate_nestedset()
|
||||
self.validate_website()
|
||||
self.validate_links_table_fieldnames()
|
||||
|
||||
if not self.is_new():
|
||||
self.before_update = frappe.get_doc('DocType', self.name)
|
||||
|
||||
if not self.is_new():
|
||||
self.setup_fields_to_fetch()
|
||||
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.default_print_format and not self.custom:
|
||||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
self.owner = 'Administrator'
|
||||
self.modified_by = 'Administrator'
|
||||
|
||||
def after_insert(self):
|
||||
# clear user cache so that on the next reload this doctype is included in boot
|
||||
clear_user_cache(frappe.session.user)
|
||||
|
||||
def set_default_in_list_view(self):
|
||||
'''Set default in-list-view for first 4 mandatory fields'''
|
||||
if not [d.fieldname for d in self.fields if d.in_list_view]:
|
||||
|
|
@ -134,6 +127,10 @@ class DocType(Document):
|
|||
if not frappe.conf.get("developer_mode") and not self.custom:
|
||||
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
self.owner = 'Administrator'
|
||||
self.modified_by = 'Administrator'
|
||||
|
||||
def setup_fields_to_fetch(self):
|
||||
'''Setup query to update values for newly set fetch values'''
|
||||
try:
|
||||
|
|
@ -192,6 +189,9 @@ class DocType(Document):
|
|||
|
||||
def validate_website(self):
|
||||
"""Ensure that website generator has field 'route'"""
|
||||
if self.route:
|
||||
self.route = self.route.strip('/')
|
||||
|
||||
if self.has_web_view:
|
||||
# route field must be present
|
||||
if not 'route' in [d.fieldname for d in self.fields]:
|
||||
|
|
@ -278,7 +278,6 @@ class DocType(Document):
|
|||
|
||||
def on_update(self):
|
||||
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
|
||||
self.delete_duplicate_custom_fields()
|
||||
try:
|
||||
frappe.db.updatedb(self.name, Meta(self))
|
||||
except Exception as e:
|
||||
|
|
@ -323,18 +322,6 @@ class DocType(Document):
|
|||
|
||||
clear_linked_doctype_cache()
|
||||
|
||||
def delete_duplicate_custom_fields(self):
|
||||
if not (frappe.db.table_exists(self.name) and frappe.db.table_exists("Custom Field")):
|
||||
return
|
||||
|
||||
fields = [d.fieldname for d in self.fields if d.fieldtype in data_fieldtypes]
|
||||
if fields:
|
||||
frappe.db.sql('''delete from
|
||||
`tabCustom Field`
|
||||
where
|
||||
dt = {0} and fieldname in ({1})
|
||||
'''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True)
|
||||
|
||||
def sync_global_search(self):
|
||||
'''If global search settings are changed, rebuild search properties for this table'''
|
||||
global_search_fields_before_update = [d.fieldname for d in
|
||||
|
|
@ -669,7 +656,7 @@ class DocType(Document):
|
|||
flags = {"flags": re.ASCII} if six.PY3 else {}
|
||||
|
||||
# a DocType name should not start or end with an empty space
|
||||
if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
|
||||
if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
|
||||
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
|
||||
|
||||
# a DocType's name should not start with a number or underscore
|
||||
|
|
@ -677,24 +664,24 @@ class DocType(Document):
|
|||
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
|
||||
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
|
||||
|
||||
def validate_links_table_fieldnames(self):
|
||||
"""Validate fieldnames in Links table"""
|
||||
if frappe.flags.in_patch: return
|
||||
if frappe.flags.in_fixtures: return
|
||||
if not self.links: return
|
||||
|
||||
for index, link in enumerate(self.links):
|
||||
meta = frappe.get_meta(link.link_doctype)
|
||||
if not meta.get_field(link.link_fieldname):
|
||||
message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
|
||||
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
|
||||
validate_route_conflict(self.doctype, self.name)
|
||||
|
||||
def validate_links_table_fieldnames(meta):
|
||||
"""Validate fieldnames in Links table"""
|
||||
if frappe.flags.in_patch: return
|
||||
if frappe.flags.in_fixtures: return
|
||||
if not meta.links: return
|
||||
|
||||
for index, link in enumerate(meta.links):
|
||||
link_meta = frappe.get_meta(link.link_doctype)
|
||||
if not link_meta.get_field(link.link_fieldname):
|
||||
message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
|
||||
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
|
||||
|
||||
def validate_fields_for_doctype(doctype):
|
||||
doc = frappe.get_doc("DocType", doctype)
|
||||
doc.delete_duplicate_custom_fields()
|
||||
validate_fields(frappe.get_meta(doctype, cached=False))
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
validate_links_table_fieldnames(meta)
|
||||
validate_fields(meta)
|
||||
|
||||
# this is separate because it is also called via custom field
|
||||
def validate_fields(meta):
|
||||
|
|
|
|||
7
frappe/core/doctype/doctype/patches/set_route.py
Normal file
7
frappe/core/doctype/doctype/patches/set_route.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import frappe
|
||||
from frappe.desk.utils import slug
|
||||
|
||||
def execute():
|
||||
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)):
|
||||
if not doctype.route:
|
||||
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False)
|
||||
|
|
@ -5,12 +5,17 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMandatoryError, DoctypeLinkError, WrongOptionsDoctypeLinkError,\
|
||||
HiddenAndMandatoryWithoutDefaultError, CannotIndexedError, InvalidFieldNameError, CannotCreateStandardDoctypeError
|
||||
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
|
||||
IllegalMandatoryError,
|
||||
DoctypeLinkError,
|
||||
WrongOptionsDoctypeLinkError,
|
||||
HiddenAndMandatoryWithoutDefaultError,
|
||||
CannotIndexedError,
|
||||
InvalidFieldNameError,
|
||||
validate_links_table_fieldnames)
|
||||
|
||||
# test_records = frappe.get_test_records('DocType')
|
||||
|
||||
|
||||
class TestDocType(unittest.TestCase):
|
||||
def test_validate_name(self):
|
||||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
|
||||
|
|
@ -459,7 +464,7 @@ class TestDocType(unittest.TestCase):
|
|||
'link_doctype': "User",
|
||||
'link_fieldname': "first_name"
|
||||
})
|
||||
doc.validate_links_table_fieldnames() # no error
|
||||
validate_links_table_fieldnames(doc) # no error
|
||||
doc.links = [] # reset links table
|
||||
|
||||
# check invalid doctype
|
||||
|
|
@ -467,7 +472,7 @@ class TestDocType(unittest.TestCase):
|
|||
'link_doctype': "User2",
|
||||
'link_fieldname': "first_name"
|
||||
})
|
||||
self.assertRaises(frappe.DoesNotExistError, doc.validate_links_table_fieldnames)
|
||||
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc)
|
||||
doc.links = [] # reset links table
|
||||
|
||||
# check invalid fieldname
|
||||
|
|
@ -475,7 +480,7 @@ class TestDocType(unittest.TestCase):
|
|||
'link_doctype': "User",
|
||||
'link_fieldname': "a_field_that_does_not_exists"
|
||||
})
|
||||
self.assertRaises(InvalidFieldNameError, doc.validate_links_table_fieldnames)
|
||||
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
|
||||
|
||||
|
||||
def new_doctype(name, unique=0, depends_on='', fields=None):
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class File(Document):
|
|||
self.add_comment_in_reference_doc('Attachment',
|
||||
_('Added {0}').format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
|
||||
"icon": ' <i class="fa fa-lock text-warning"></i>' if self.is_private else "",
|
||||
"file_url": quote(self.file_url) if self.file_url else self.file_name,
|
||||
"file_url": quote(frappe.safe_encode(self.file_url)) if self.file_url else self.file_name,
|
||||
"file_name": self.file_name or self.file_url
|
||||
})))
|
||||
|
||||
|
|
@ -456,7 +456,7 @@ class File(Document):
|
|||
def save_file(self, content=None, decode=False, ignore_existing_file_check=False):
|
||||
file_exists = False
|
||||
self.content = content
|
||||
|
||||
|
||||
if decode:
|
||||
if isinstance(content, text_type):
|
||||
self.content = content.encode("utf-8")
|
||||
|
|
@ -467,19 +467,19 @@ class File(Document):
|
|||
|
||||
if not self.is_private:
|
||||
self.is_private = 0
|
||||
|
||||
|
||||
self.content_type = mimetypes.guess_type(self.file_name)[0]
|
||||
|
||||
|
||||
self.file_size = self.check_max_file_size()
|
||||
|
||||
|
||||
if (
|
||||
self.content_type and "image" in self.content_type
|
||||
and frappe.get_system_settings("strip_exif_metadata_from_uploaded_images")
|
||||
):
|
||||
self.content = strip_exif_data(self.content, self.content_type)
|
||||
self.content = strip_exif_data(self.content, self.content_type)
|
||||
|
||||
self.content_hash = get_content_hash(self.content)
|
||||
|
||||
|
||||
duplicate_file = None
|
||||
|
||||
# check if a file exists with the same content hash and is also in the same folder (public or private)
|
||||
|
|
@ -940,10 +940,33 @@ def validate_filename(filename):
|
|||
return fname
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_files_in_folder(folder):
|
||||
return frappe.db.get_all('File',
|
||||
def get_files_in_folder(folder, start=0, page_length=20):
|
||||
start = cint(start)
|
||||
page_length = cint(page_length)
|
||||
|
||||
files = frappe.db.get_all('File',
|
||||
{ 'folder': folder },
|
||||
['name', 'file_name', 'file_url', 'is_folder', 'modified']
|
||||
['name', 'file_name', 'file_url', 'is_folder', 'modified'],
|
||||
start=start,
|
||||
page_length=page_length + 1
|
||||
)
|
||||
return {
|
||||
'files': files[:page_length],
|
||||
'has_more': len(files) > page_length
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_files_by_search_text(text):
|
||||
if not text:
|
||||
return []
|
||||
|
||||
text = '%' + cstr(text).lower() + '%'
|
||||
return frappe.db.get_all('File',
|
||||
fields=['name', 'file_name', 'file_url', 'is_folder', 'modified'],
|
||||
filters={'is_folder': False},
|
||||
or_filters={'file_name': ('like', text), 'file_url': text, 'name': ('like', text)},
|
||||
order_by='modified desc',
|
||||
limit=20
|
||||
)
|
||||
|
||||
def update_existing_file_docs(doc):
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def has_unseen_error_log(user):
|
|||
def _get_response(show_alert=True):
|
||||
return {
|
||||
'show_alert': True,
|
||||
'message': _("You have unseen {0}").format('<a href="/desk#List/Error%20Log/List"> Error Logs </a>')
|
||||
'message': _("You have unseen {0}").format('<a href="/app/List/Error%20Log/List"> Error Logs </a>')
|
||||
}
|
||||
|
||||
if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
"link_fieldname": "module"
|
||||
},
|
||||
{
|
||||
"link_doctype": "Desk Page",
|
||||
"link_doctype": "Workspace",
|
||||
"link_fieldname": "module"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ class NavbarSettings(Document):
|
|||
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
|
||||
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_app_logo():
|
||||
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo')
|
||||
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
|
||||
if not app_logo:
|
||||
app_logo = frappe.get_hooks('app_logo_url')[-1]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from frappe.build import html_to_js_template
|
|||
from frappe.model.utils import render_include
|
||||
from frappe import conf, _, safe_decode
|
||||
from frappe.desk.form.meta import get_code_files_via_hooks, get_js
|
||||
from frappe.desk.utils import validate_route_conflict
|
||||
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
|
||||
from six import text_type
|
||||
|
||||
|
|
@ -33,6 +34,8 @@ class Page(Document):
|
|||
self.name += '-' + str(cnt)
|
||||
|
||||
def validate(self):
|
||||
validate_route_conflict(self.doctype, self.name)
|
||||
|
||||
if self.is_new() and not getattr(conf,'developer_mode', 0):
|
||||
frappe.throw(_("Not in Developer Mode"))
|
||||
|
||||
|
|
|
|||
5
frappe/core/doctype/page/patches/drop_unused_pages.py
Normal file
5
frappe/core/doctype/page/patches/drop_unused_pages.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
for name in ('desktop', 'space'):
|
||||
frappe.delete_doc('Page', name)
|
||||
|
|
@ -8,4 +8,6 @@ import unittest
|
|||
test_records = frappe.get_test_records('Page')
|
||||
|
||||
class TestPage(unittest.TestCase):
|
||||
pass
|
||||
def test_naming(self):
|
||||
self.assertRaises(frappe.NameError, frappe.get_doc(dict(doctype='Page', page_name='DocType', module='Core')).insert)
|
||||
self.assertRaises(frappe.NameError, frappe.get_doc(dict(doctype='Page', page_name='Settings', module='Core')).insert)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import frappe
|
||||
from ..role import desk_properties
|
||||
|
||||
def execute():
|
||||
frappe.reload_doctype('role')
|
||||
for role in frappe.get_all('Role', ['name', 'desk_access']):
|
||||
role_doc = frappe.get_doc('Role', role.name)
|
||||
for key in desk_properties:
|
||||
role_doc.set(key, role_doc.desk_access)
|
||||
role_doc.save()
|
||||
|
|
@ -13,7 +13,19 @@
|
|||
"column_break_4",
|
||||
"disabled",
|
||||
"desk_access",
|
||||
"two_factor_auth"
|
||||
"two_factor_auth",
|
||||
"navigation_settings_section",
|
||||
"search_bar",
|
||||
"notifications",
|
||||
"chat",
|
||||
"list_settings_section",
|
||||
"list_sidebar",
|
||||
"bulk_actions",
|
||||
"view_switcher",
|
||||
"form_settings_section",
|
||||
"form_sidebar",
|
||||
"timeline",
|
||||
"dashboard"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -60,12 +72,82 @@
|
|||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "navigation_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Navigation Settings"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "search_bar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Search Bar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Chat"
|
||||
},
|
||||
{
|
||||
"fieldname": "list_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "List Settings"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "list_sidebar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sidebar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "bulk_actions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bulk Actions"
|
||||
},
|
||||
{
|
||||
"fieldname": "form_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Form Settings"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "form_sidebar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sidebar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "timeline",
|
||||
"fieldtype": "Check",
|
||||
"label": "Timeline"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "dashboard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Dashboard"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "view_switcher",
|
||||
"fieldtype": "Check",
|
||||
"label": "View Switcher"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "notifications",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notifications"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bookmark",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-06 15:42:59.036960",
|
||||
"modified": "2020-12-03 14:08:38.181035",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Role",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import frappe
|
|||
|
||||
from frappe.model.document import Document
|
||||
|
||||
desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
|
||||
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
|
||||
|
||||
class Role(Document):
|
||||
def before_rename(self, old, new, merge=False):
|
||||
if old in ("Guest", "Administrator", "System Manager", "All"):
|
||||
|
|
@ -16,11 +19,28 @@ class Role(Document):
|
|||
|
||||
def validate(self):
|
||||
if self.disabled:
|
||||
if self.name in ("Guest", "Administrator", "System Manager", "All"):
|
||||
frappe.throw(frappe._("Standard roles cannot be disabled"))
|
||||
else:
|
||||
frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
|
||||
frappe.clear_cache()
|
||||
self.disable_role()
|
||||
else:
|
||||
self.set_desk_properties()
|
||||
|
||||
def disable_role(self):
|
||||
if self.name in ("Guest", "Administrator", "System Manager", "All"):
|
||||
frappe.throw(frappe._("Standard roles cannot be disabled"))
|
||||
else:
|
||||
self.remove_roles()
|
||||
|
||||
def set_desk_properties(self):
|
||||
# set if desk_access is not allowed, unset all desk properties
|
||||
if self.name == 'Guest':
|
||||
self.desk_access = 0
|
||||
|
||||
if not self.desk_access:
|
||||
for key in desk_properties:
|
||||
self.set(key, 0)
|
||||
|
||||
def remove_roles(self):
|
||||
frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
|
||||
frappe.clear_cache()
|
||||
|
||||
def on_update(self):
|
||||
'''update system user desk access if this has changed in this update'''
|
||||
|
|
|
|||
|
|
@ -3,32 +3,32 @@
|
|||
|
||||
frappe.ui.form.on('Role Permission for Page and Report', {
|
||||
setup: function(frm) {
|
||||
frm.trigger("set_queries")
|
||||
frm.trigger("set_queries");
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.role_area.hide();
|
||||
frm.events.add_custom_buttons(frm);
|
||||
frm.events.setup_buttons(frm);
|
||||
},
|
||||
|
||||
add_custom_buttons: function(frm) {
|
||||
setup_buttons: function(frm) {
|
||||
frm.clear_custom_buttons();
|
||||
if(frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) {
|
||||
frm.page.clear_actions();
|
||||
if (frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) {
|
||||
frm.add_custom_button(__("Reset to defaults"), function() {
|
||||
frm.trigger("reset_roles");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Update"), function() {
|
||||
frm.page.set_primary_action(__("Update"), () => {
|
||||
frm.trigger("update_report_page_data");
|
||||
}).addClass('btn-primary');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
if(!frm.roles_editor) {
|
||||
frm.role_area = $('<div style="min-height: 300px">')
|
||||
.appendTo(frm.fields_dict.roles_html.wrapper);
|
||||
if (!frm.roles_editor) {
|
||||
frm.role_area = $(frm.fields_dict.roles_html.wrapper);
|
||||
frm.roles_editor = new frappe.RoleEditor(frm.role_area, frm);
|
||||
}
|
||||
},
|
||||
|
|
@ -54,17 +54,17 @@ frappe.ui.form.on('Role Permission for Page and Report', {
|
|||
},
|
||||
|
||||
page: function(frm) {
|
||||
frm.events.add_custom_buttons(frm);
|
||||
if(frm.doc.page) {
|
||||
frm.events.setup_buttons(frm);
|
||||
if (frm.doc.page) {
|
||||
frm.trigger("set_report_page_data");
|
||||
} else {
|
||||
frm.trigger("set_role_for");
|
||||
}
|
||||
},
|
||||
|
||||
report: function(frm){
|
||||
frm.events.add_custom_buttons(frm);
|
||||
if(frm.doc.report) {
|
||||
report: function(frm) {
|
||||
frm.events.setup_buttons(frm);
|
||||
if (frm.doc.report) {
|
||||
frm.trigger("set_report_page_data");
|
||||
} else {
|
||||
frm.trigger("set_role_for");
|
||||
|
|
|
|||
|
|
@ -3,20 +3,18 @@
|
|||
|
||||
frappe.ui.form.on('Role Profile', {
|
||||
refresh: function(frm) {
|
||||
if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
||||
if(!frm.roles_editor) {
|
||||
var role_area = $('<div style="min-height: 300px">')
|
||||
.appendTo(frm.fields_dict.roles_html.wrapper);
|
||||
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
||||
if (!frm.roles_editor) {
|
||||
const role_area = $(frm.fields_dict.roles_html.wrapper);
|
||||
frm.roles_editor = new frappe.RoleEditor(role_area, frm);
|
||||
frm.roles_editor.show();
|
||||
} else {
|
||||
frm.roles_editor.show();
|
||||
}
|
||||
frm.roles_editor.show();
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
if(frm.roles_editor) {
|
||||
if (frm.roles_editor) {
|
||||
frm.roles_editor.set_roles_in_table();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe, json
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -11,12 +12,13 @@ from datetime import datetime
|
|||
from croniter import croniter
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
|
||||
|
||||
class ScheduledJobType(Document):
|
||||
def autoname(self):
|
||||
self.name = '.'.join(self.method.split('.')[-2:])
|
||||
self.name = ".".join(self.method.split(".")[-2:])
|
||||
|
||||
def validate(self):
|
||||
if self.frequency != 'All':
|
||||
if self.frequency != "All":
|
||||
# force logging for all events other than continuous ones (ALL)
|
||||
self.create_log = 1
|
||||
|
||||
|
|
@ -84,7 +86,7 @@ class ScheduledJobType(Document):
|
|||
|
||||
def log_status(self, status):
|
||||
# log file
|
||||
frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
|
||||
frappe.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {frappe.local.site}")
|
||||
self.update_scheduler_log(status)
|
||||
|
||||
def update_scheduler_log(self, status):
|
||||
|
|
@ -111,28 +113,29 @@ class ScheduledJobType(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def execute_event(doc):
|
||||
frappe.only_for('System Manager')
|
||||
def execute_event(doc: str):
|
||||
frappe.only_for("System Manager")
|
||||
doc = json.loads(doc)
|
||||
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True)
|
||||
frappe.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True)
|
||||
return doc
|
||||
|
||||
|
||||
def run_scheduled_job(job_type):
|
||||
'''This is a wrapper function that runs a hooks.scheduler_events method'''
|
||||
def run_scheduled_job(job_type: str):
|
||||
"""This is a wrapper function that runs a hooks.scheduler_events method"""
|
||||
try:
|
||||
frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute()
|
||||
frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute()
|
||||
except Exception:
|
||||
print(frappe.get_traceback())
|
||||
|
||||
|
||||
def sync_jobs(hooks=None):
|
||||
def sync_jobs(hooks: Dict = None):
|
||||
frappe.reload_doc("core", "doctype", "scheduled_job_type")
|
||||
scheduler_events = hooks or frappe.get_hooks("scheduler_events")
|
||||
all_events = insert_events(scheduler_events)
|
||||
clear_events(all_events)
|
||||
|
||||
|
||||
def insert_events(scheduler_events):
|
||||
def insert_events(scheduler_events: Dict) -> List:
|
||||
cron_jobs, event_jobs = [], []
|
||||
for event_type in scheduler_events:
|
||||
events = scheduler_events.get(event_type)
|
||||
|
|
@ -144,7 +147,7 @@ def insert_events(scheduler_events):
|
|||
return cron_jobs + event_jobs
|
||||
|
||||
|
||||
def insert_cron_jobs(events):
|
||||
def insert_cron_jobs(events: Dict) -> List:
|
||||
cron_jobs = []
|
||||
for cron_format in events:
|
||||
for event in events.get(cron_format):
|
||||
|
|
@ -153,25 +156,29 @@ def insert_cron_jobs(events):
|
|||
return cron_jobs
|
||||
|
||||
|
||||
def insert_event_jobs(events, event_type):
|
||||
def insert_event_jobs(events: List, event_type: str) -> List:
|
||||
event_jobs = []
|
||||
for event in events:
|
||||
event_jobs.append(event)
|
||||
frequency = event_type.replace('_', ' ').title()
|
||||
frequency = event_type.replace("_", " ").title()
|
||||
insert_single_event(frequency, event)
|
||||
return event_jobs
|
||||
|
||||
|
||||
def insert_single_event(frequency, event, cron_format=None):
|
||||
def insert_single_event(frequency: str, event: str, cron_format: str = None):
|
||||
cron_expr = {"cron_format": cron_format} if cron_format else {}
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Scheduled Job Type",
|
||||
"method": event,
|
||||
"cron_format": cron_format,
|
||||
"frequency": frequency
|
||||
})
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Scheduled Job Type",
|
||||
"method": event,
|
||||
"cron_format": cron_format,
|
||||
"frequency": frequency,
|
||||
}
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }):
|
||||
if not frappe.db.exists(
|
||||
"Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr}
|
||||
):
|
||||
try:
|
||||
doc.insert()
|
||||
except frappe.DuplicateEntryError:
|
||||
|
|
@ -179,7 +186,12 @@ def insert_single_event(frequency, event, cron_format=None):
|
|||
doc.insert()
|
||||
|
||||
|
||||
def clear_events(all_events):
|
||||
for event in frappe.get_all("Scheduled Job Type", ("name", "method")):
|
||||
if event.method not in all_events:
|
||||
def clear_events(all_events: List):
|
||||
for event in frappe.get_all(
|
||||
"Scheduled Job Type", fields=["name", "method", "server_script"]
|
||||
):
|
||||
is_server_script = event.server_script
|
||||
is_defined_in_hooks = event.method in all_events
|
||||
|
||||
if not (is_defined_in_hooks or is_server_script):
|
||||
frappe.delete_doc("Scheduled Job Type", event.name)
|
||||
|
|
|
|||
|
|
@ -6,46 +6,11 @@ frappe.ui.form.on('Server Script', {
|
|||
frm.trigger('setup_help');
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled) {
|
||||
frm.add_custom_button('Schedule Script', function() {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: "Schedule Script Execution",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "event_type",
|
||||
label: __('Select Event Type'),
|
||||
fieldtype: "Select",
|
||||
options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
|
||||
},
|
||||
],
|
||||
primary_action_label: __('Schedule Script'),
|
||||
primary_action: () => {
|
||||
d.get_primary_btn().attr('disabled', true);
|
||||
var data = d.get_values();
|
||||
d.hide();
|
||||
if(data) {
|
||||
frm.events.schedule_script(frm, data);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
});
|
||||
if (frm.doc.script_type != 'Scheduler Event') {
|
||||
frm.dashboard.hide();
|
||||
}
|
||||
},
|
||||
|
||||
schedule_script(frm, data) {
|
||||
frm.call({
|
||||
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
|
||||
args: {
|
||||
'script_name': frm.doc.name,
|
||||
'frequency': data.event_type
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setup_help(frm) {
|
||||
frm.get_field('help_html').html(`
|
||||
<h4>DocType Event</h4>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"field_order": [
|
||||
"script_type",
|
||||
"reference_doctype",
|
||||
"event_frequency",
|
||||
"doctype_event",
|
||||
"api_method",
|
||||
"allow_guest",
|
||||
|
|
@ -84,11 +85,24 @@
|
|||
{
|
||||
"fieldname": "help_html",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type == \"Scheduler Event\"",
|
||||
"fieldname": "event_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Event Frequency",
|
||||
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
|
||||
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-03 18:50:14.767595",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Scheduled Job Type",
|
||||
"link_fieldname": "server_script"
|
||||
}
|
||||
],
|
||||
"modified": "2021-02-18 12:36:19.803425",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue