Merge branch 'develop' into phone_field_control
This commit is contained in:
commit
579d0643c4
436 changed files with 13114 additions and 7399 deletions
|
|
@ -13,3 +13,9 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
|
|||
|
||||
# Updating license headers
|
||||
34460265554242a8d05fb09f049033b1117e1a2b
|
||||
|
||||
# Refactor "not a in b" -> "a not in b"
|
||||
745297a49d516e5e3c4bb3e1b0c4235e7d31165d
|
||||
|
||||
# Clean up whitespace
|
||||
b2fc959307c7c79f5584625569d5aed04133ba13
|
||||
|
|
|
|||
6
.github/helper/install.sh
vendored
6
.github/helper/install.sh
vendored
|
|
@ -50,7 +50,9 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f
|
|||
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
bench setup requirements --dev
|
||||
|
||||
if [ "$TYPE" == "ui" ]; then sed -i 's/^web: bench serve/web: bench serve --with-coverage/g' Procfile; fi
|
||||
|
||||
# install node-sass which is required for website theme test
|
||||
cd ./apps/frappe || exit
|
||||
|
|
@ -60,4 +62,4 @@ cd ../..
|
|||
bench start &
|
||||
bench --site test_site reinstall --yes
|
||||
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
|
||||
CI=Yes bench build --app frappe
|
||||
if [ "$TYPE" == "server" ]; then CI=Yes bench build --app frappe; fi
|
||||
|
|
|
|||
14
.github/helper/roulette.py
vendored
14
.github/helper/roulette.py
vendored
|
|
@ -41,6 +41,7 @@ if __name__ == "__main__":
|
|||
# this is a push build, run all builds
|
||||
if not pr_number:
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
os.system('echo "::set-output name=build-server::strawberry"')
|
||||
sys.exit(0)
|
||||
|
||||
files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
|
||||
|
|
@ -52,7 +53,8 @@ if __name__ == "__main__":
|
|||
ci_files_changed = any(f for f in files_list if is_ci(f))
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
updated_py_file_count = len(list(filter(is_py, files_list)))
|
||||
only_py_changed = updated_py_file_count == len(files_list)
|
||||
|
||||
if ci_files_changed:
|
||||
print("CI related files were updated, running all build processes.")
|
||||
|
|
@ -65,8 +67,12 @@ if __name__ == "__main__":
|
|||
print("Only Frontend code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
elif build_type == "ui":
|
||||
if only_py_changed:
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
elif updated_py_file_count > 0:
|
||||
# both frontend and backend code were updated
|
||||
os.system('echo "::set-output name=build-server::strawberry"')
|
||||
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
name: Semgrep
|
||||
name: Linters
|
||||
|
||||
on:
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
|
||||
linters:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
18
.github/workflows/ui-tests.yml
vendored
18
.github/workflows/ui-tests.yml
vendored
|
|
@ -137,10 +137,16 @@ jobs:
|
|||
|
||||
- name: UI Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
||||
- name: Stop server
|
||||
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
|
||||
run: |
|
||||
ps -ef | grep "frappe serve" | awk '{print $2}' | xargs kill -s SIGINT 2> /dev/null || true
|
||||
sleep 5
|
||||
|
||||
- name: Check If Coverage Report Exists
|
||||
id: check_coverage
|
||||
uses: andstor/file-existence-action@v1
|
||||
|
|
@ -156,3 +162,13 @@ jobs:
|
|||
directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
|
||||
verbose: true
|
||||
flags: ui-tests
|
||||
|
||||
- name: Upload Server Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
flags: server
|
||||
|
|
|
|||
|
|
@ -48,3 +48,7 @@ pull_request_rules:
|
|||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
|
|
|
|||
23
.pre-commit-config.yaml
Normal file
23
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "frappe.*"
|
||||
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
|
||||
- id: check-yaml
|
||||
- id: no-commit-to-branch
|
||||
args: ['--branch', 'develop']
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
submodules: false
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/frappe/frappe">
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj&flag=server"/>
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ codecov:
|
|||
|
||||
coverage:
|
||||
status:
|
||||
patch: off
|
||||
project:
|
||||
default: false
|
||||
server:
|
||||
|
|
|
|||
30
cypress/fixtures/child_table_doctype.js
Normal file
30
cypress/fixtures/child_table_doctype.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export default {
|
||||
name: "Child Table Doctype",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
autoname: "field:title",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "title",
|
||||
fieldtype: "Data",
|
||||
in_list_view: 1,
|
||||
label: "Title",
|
||||
unique: 1
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
istable: 1,
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
naming_rule: "By fieldname",
|
||||
owner: "Administrator",
|
||||
permissions: [],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
59
cypress/fixtures/child_table_doctype_1.js
Normal file
59
cypress/fixtures/child_table_doctype_1.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
export default {
|
||||
name: "Child Table Doctype 1",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
autoname: "format: Test-{####}",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "data",
|
||||
fieldtype: "Data",
|
||||
in_list_view: 1,
|
||||
label: "Data"
|
||||
},
|
||||
{
|
||||
fieldname: "barcode",
|
||||
fieldtype: "Barcode",
|
||||
in_list_view: 1,
|
||||
label: "Barcode"
|
||||
},
|
||||
{
|
||||
fieldname: "check",
|
||||
fieldtype: "Check",
|
||||
in_list_view: 1,
|
||||
label: "Check"
|
||||
},
|
||||
{
|
||||
fieldname: "rating",
|
||||
fieldtype: "Rating",
|
||||
in_list_view: 1,
|
||||
label: "Rating"
|
||||
},
|
||||
{
|
||||
fieldname: "duration",
|
||||
fieldtype: "Duration",
|
||||
in_list_view: 1,
|
||||
label: "Duration"
|
||||
},
|
||||
{
|
||||
fieldname: "date",
|
||||
fieldtype: "Date",
|
||||
in_list_view: 1,
|
||||
label: "Date"
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
istable: 1,
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
naming_rule: "By fieldname",
|
||||
owner: "Administrator",
|
||||
permissions: [],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
45
cypress/fixtures/doctype_to_link.js
Normal file
45
cypress/fixtures/doctype_to_link.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
export default {
|
||||
name: "Doctype to Link",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
naming_rule: "By fieldname",
|
||||
autoname: "field:title",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
links: [
|
||||
{
|
||||
"group": "Child Doctype",
|
||||
"link_doctype": "Doctype With Child Table",
|
||||
"link_fieldname": "title"
|
||||
}
|
||||
],
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
owner: "Administrator",
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
52
cypress/fixtures/doctype_with_child_table.js
Normal file
52
cypress/fixtures/doctype_with_child_table.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
export default {
|
||||
name: "Doctype With Child Table",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
autoname: "field:title",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "title",
|
||||
fieldtype: "Data",
|
||||
label: "Title",
|
||||
unique: 1
|
||||
},
|
||||
{
|
||||
fieldname: "child_table",
|
||||
fieldtype: "Table",
|
||||
label: "Child Table",
|
||||
options: "Child Table Doctype",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: "child_table_1",
|
||||
fieldtype: "Table",
|
||||
label: "Child Table 1",
|
||||
options: "Child Table Doctype 1"
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
naming_rule: "By fieldname",
|
||||
owner: "Administrator",
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
57
cypress/integration/control_autocomplete.js
Normal file
57
cypress/integration/control_autocomplete.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
context('Control Autocomplete', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_autocomplete(options) {
|
||||
cy.visit('/app/website');
|
||||
return cy.dialog({
|
||||
title: 'Autocomplete',
|
||||
fields: [
|
||||
{
|
||||
'label': 'Select an option',
|
||||
'fieldname': 'autocomplete',
|
||||
'fieldtype': 'Autocomplete',
|
||||
'options': options || ['Option 1', 'Option 2', 'Option 3'],
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set the valid value', () => {
|
||||
get_dialog_with_autocomplete().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
|
||||
cy.wait(1000);
|
||||
cy.get('@input').type('2', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('autocomplete');
|
||||
expect(value).to.eq('Option 2');
|
||||
dialog.clear();
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the valid value with different label', () => {
|
||||
const options_with_label = [
|
||||
{ label: "Option 1", value: "option_1" },
|
||||
{ label: "Option 2", value: "option_2" }
|
||||
];
|
||||
get_dialog_with_autocomplete(options_with_label).as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
|
||||
cy.get('@input').type('2', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('autocomplete');
|
||||
expect(value).to.eq('option_2');
|
||||
dialog.clear();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -21,7 +21,6 @@ context('Control Barcode', () => {
|
|||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
|
||||
|
|
@ -38,7 +37,6 @@ context('Control Barcode', () => {
|
|||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
|
|
|
|||
|
|
@ -19,18 +19,18 @@ context('Control Icon', () => {
|
|||
get_dialog_with_icon().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click();
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'active');
|
||||
cy.get('.icon-picker .icon-wrapper[id=heart-active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart-active');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('active');
|
||||
expect(value).to.equal('heart-active');
|
||||
});
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=resting]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'resting');
|
||||
cy.get('.icon-picker .icon-wrapper[id=heart]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('resting');
|
||||
expect(value).to.equal('heart');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,23 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
|
||||
});
|
||||
|
||||
it("should be possible set empty value explicitly", () => {
|
||||
get_dialog_with_link().as("dialog");
|
||||
|
||||
cy.intercept("POST", "/api/method/frappe.client.validate_link").as("validate_link");
|
||||
|
||||
cy.get(".frappe-control[data-fieldname=link] input")
|
||||
.type(" ", { delay: 100 })
|
||||
.blur();
|
||||
cy.wait("@validate_link");
|
||||
cy.get(".frappe-control[data-fieldname=link] input").should("have.value", "");
|
||||
cy.window()
|
||||
.its("cur_dialog")
|
||||
.then((dialog) => {
|
||||
expect(dialog.get_value("link")).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
|
|
@ -78,7 +95,52 @@ context('Control Link', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should fetch valid value', () => {
|
||||
it('show title field in link', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.insert_doc("Property Setter", {
|
||||
"doctype": "Property Setter",
|
||||
"doc_type": "ToDo",
|
||||
"property": "show_title_field_in_link",
|
||||
"property_type": "Check",
|
||||
"doctype_or_field": "DocType",
|
||||
"value": "1"
|
||||
}, true);
|
||||
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
if (!frappe.boot) {
|
||||
frappe.boot = {
|
||||
link_title_doctypes: ['ToDo']
|
||||
};
|
||||
} else {
|
||||
frappe.boot.link_title_doctypes = ['ToDo'];
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
cy.get('@input').type('todo for link');
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname=link] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
cy.get('@todos').then(todos => {
|
||||
let field = dialog.get_field('link');
|
||||
let value = field.get_value();
|
||||
let label = field.get_label_value();
|
||||
|
||||
expect(value).to.eq(todos[0]);
|
||||
expect(label).to.eq('this is a test todo for link');
|
||||
|
||||
cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update dependant fields (via fetch_from)', () => {
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.visit(`/app/todo/${todos[0]}`);
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
|
@ -89,7 +151,67 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', 'Administrator'
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "Administrator");
|
||||
|
||||
// invalid input
|
||||
cy.get('@input').clear().type('invalid input', {delay: 100}).blur();
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', ''
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", null);
|
||||
|
||||
// set valid value again
|
||||
cy.get('@input').clear().type('Administrator', {delay: 100}).blur();
|
||||
cy.wait('@validate_link');
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "Administrator");
|
||||
|
||||
// clear input
|
||||
cy.get('@input').clear().blur();
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', ''
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "");
|
||||
});
|
||||
});
|
||||
|
||||
it("should set default values", () => {
|
||||
cy.insert_doc("Property Setter", {
|
||||
"doctype_or_field": "DocField",
|
||||
"doc_type": "ToDo",
|
||||
"field_name": "assigned_by",
|
||||
"property": "default",
|
||||
"property_type": "Text",
|
||||
"value": "Administrator"
|
||||
}, true);
|
||||
cy.reload();
|
||||
cy.new_form("ToDo");
|
||||
cy.fill_field("description", "new", "Text Editor");
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.wait("@save_form");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain", "Administrator"
|
||||
);
|
||||
// if user clears default value explicitly, system should not reset default again
|
||||
cy.get_field("assigned_by").clear().blur();
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.wait("@save_form");
|
||||
cy.get_field("assigned_by").should("have.value", "");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain", ""
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
|
||||
import child_table_doctype from '../fixtures/child_table_doctype';
|
||||
import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
|
||||
import doctype_to_link from '../fixtures/doctype_to_link';
|
||||
const doctype_to_link_name = doctype_to_link.name;
|
||||
const child_table_doctype_name = child_table_doctype.name;
|
||||
|
||||
context('Dashboard links', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.insert_doc('DocType', child_table_doctype, true);
|
||||
cy.insert_doc('DocType', child_table_doctype_1, true);
|
||||
cy.insert_doc('DocType', doctype_with_child_table, true);
|
||||
cy.insert_doc('DocType', doctype_to_link, true);
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall("frappe.tests.ui_test_helpers.update_child_table", {
|
||||
name: child_table_doctype_name
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
|
||||
|
|
@ -62,4 +78,14 @@ context('Dashboard links', () => {
|
|||
cy.findByText('Website Analytics');
|
||||
});
|
||||
});
|
||||
|
||||
it('check if child table is populated with linked field on creation from dashboard link', () => {
|
||||
cy.new_form(doctype_to_link_name);
|
||||
cy.fill_field("title", "Test Linking");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
|
||||
cy.get('.document-link .btn-new').click();
|
||||
cy.get('.frappe-control[data-fieldname="child_table"] .rows .data-row .col[data-fieldname="doctype_to_link"]')
|
||||
.should('contain.text', 'Test Linking');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ context('Control Date, Time and DateTime', () => {
|
|||
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
|
||||
}
|
||||
];
|
||||
|
||||
datetime_formats.forEach(d => {
|
||||
it(`test datetime format ${d.date_format} ${d.time_format}`, () => {
|
||||
cy.set_value('System Settings', 'System Settings', {
|
||||
|
|
|
|||
|
|
@ -55,10 +55,31 @@ context('Depends On', () => {
|
|||
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
|
||||
'options': "Child Test Depends On"
|
||||
},
|
||||
{
|
||||
"label": "Dependent Tab",
|
||||
"fieldname": "dependent_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"depends_on": "eval:doc.test_field=='Show Tab'"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_section",
|
||||
"fieldtype": "Section Break",
|
||||
},
|
||||
{
|
||||
"label": "Field in Tab",
|
||||
"fieldname": "field_in_tab",
|
||||
"fieldtype": "Data",
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should show the tab on other setting field value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Show Tab');
|
||||
cy.get('body').click();
|
||||
cy.findByRole("tab", {name: "Dependent Tab"}).should('be.visible');
|
||||
});
|
||||
it('should set the field as mandatory depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Some Value');
|
||||
|
|
|
|||
92
cypress/integration/grid.js
Normal file
92
cypress/integration/grid.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
context('Grid', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
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('update docfield property using update_docfield_property', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.update_docfield_property("is_primary_phone", "hidden", true);
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
});
|
||||
});
|
||||
it('update docfield property using toggle_display', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.toggle_display("is_primary_mobile_no", false);
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
});
|
||||
});
|
||||
it('update docfield property using toggle_enable', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.toggle_enable("phone", false);
|
||||
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input');
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input');
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
});
|
||||
});
|
||||
it('update docfield property using toggle_reqd', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.toggle_reqd("phone", false);
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get_field("phone").as('phone-field');
|
||||
cy.get('@phone-field').focus().clear().wait(500).blur();
|
||||
cy.get('@phone-field').should("not.have.class", "has-error");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get_field("phone").as('phone-field');
|
||||
cy.get('@phone-field').focus().clear().wait(500).blur();
|
||||
cy.get('@phone-field').should("not.have.class", "has-error");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
107
cypress/integration/grid_search.js
Normal file
107
cypress/integration/grid_search.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
|
||||
import child_table_doctype from '../fixtures/child_table_doctype';
|
||||
import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
|
||||
const doctype_with_child_table_name = doctype_with_child_table.name;
|
||||
|
||||
context('Grid Search', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.insert_doc('DocType', child_table_doctype, true);
|
||||
cy.insert_doc('DocType', child_table_doctype_1, true);
|
||||
cy.insert_doc('DocType', doctype_with_child_table, true);
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", {
|
||||
name: doctype_with_child_table_name
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Test search row visibility', () => {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.model.user_settings.save('Doctype With Child Table', 'GridView', {
|
||||
'Child Table Doctype 1': [
|
||||
{'fieldname': 'data', 'columns': 2},
|
||||
{'fieldname': 'barcode', 'columns': 1},
|
||||
{'fieldname': 'check', 'columns': 1},
|
||||
{'fieldname': 'rating', 'columns': 2},
|
||||
{'fieldname': 'duration', 'columns': 2},
|
||||
{'fieldname': 'date', 'columns': 2}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
|
||||
|
||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
|
||||
cy.get('@table').find('.grid-row-check:last').click();
|
||||
cy.get('@table').find('.grid-footer').contains('Delete').click();
|
||||
cy.get('.grid-heading-row .grid-row .search').should('not.exist');
|
||||
});
|
||||
|
||||
it('test search field for different fieldtypes', () => {
|
||||
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
|
||||
|
||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
|
||||
|
||||
// Index Column
|
||||
cy.get('@table').find('.grid-heading-row .row-index.search input').type('3');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
|
||||
cy.get('@table').find('.grid-heading-row .row-index.search input').clear();
|
||||
|
||||
// Data Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear();
|
||||
|
||||
// Barcode Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear();
|
||||
|
||||
// Check Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();
|
||||
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();
|
||||
|
||||
// Rating Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear();
|
||||
|
||||
// Duration Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear();
|
||||
|
||||
// Date Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear();
|
||||
});
|
||||
|
||||
it('test with multiple filter', () => {
|
||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
|
||||
|
||||
// Data Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10);
|
||||
|
||||
// Barcode Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8);
|
||||
|
||||
// Duration Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5);
|
||||
|
||||
// Date Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('02-');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
38
cypress/integration/list_paging.js
Normal file
38
cypress/integration/list_paging.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
context('List Paging', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_todo_records");
|
||||
});
|
||||
});
|
||||
|
||||
it('test load more with count selection buttons', () => {
|
||||
cy.visit('/app/todo/view/report');
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
|
||||
|
||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
|
||||
|
||||
// check if refresh works after load more
|
||||
cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
|
||||
|
||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,7 @@ context('List View', () => {
|
|||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible');
|
||||
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh');
|
||||
cy.wait(3000); // wait before you hit another refresh
|
||||
cy.get('button[data-original-title="Refresh"]').click();
|
||||
cy.wait('@list-refresh');
|
||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
|
||||
|
|
|
|||
|
|
@ -77,11 +77,11 @@ context('MultiSelectDialog', () => {
|
|||
|
||||
it('tests more button', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="more_btn"]`)
|
||||
.get(`.frappe-control[data-fieldname="more_child_btn"]`)
|
||||
.should('exist')
|
||||
.as('more-btn');
|
||||
|
||||
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
|
||||
cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
|
||||
expect($rows).to.have.length(20);
|
||||
});
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ context('MultiSelectDialog', () => {
|
|||
cy.get('@more-btn').find('button').click({force: true});
|
||||
cy.wait('@get-more-records');
|
||||
|
||||
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
|
||||
cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
|
||||
if ($rows.length <= 20) {
|
||||
throw new Error("More button doesn't work");
|
||||
}
|
||||
|
|
|
|||
22
cypress/integration/number_card.js
Normal file
22
cypress/integration/number_card.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
context('Number Card', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Check filter populate for child table doctype', () => {
|
||||
cy.visit('/app/number-card/new-number-card-1');
|
||||
cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
|
||||
|
||||
cy.get_field('document_type', 'Link');
|
||||
cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
|
||||
cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
|
||||
|
||||
cy.fill_field('label', 'Test Number Card', 'Data');
|
||||
|
||||
cy.get('[data-fieldname="filters_json"]').click().wait(200);
|
||||
cy.get('.modal-body .filter-action-buttons .add-filter').click();
|
||||
cy.get('.modal-body .fieldname-select-area').click();
|
||||
cy.get('.modal-actions .btn-modal-close').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -7,34 +7,37 @@ context('Report View', () => {
|
|||
cy.visit('/app/website');
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
cy.clear_cache();
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.insert_doc(doctype_name, {
|
||||
'title': 'Doc 1',
|
||||
'description': 'Random Text',
|
||||
'enabled': 0,
|
||||
// submit document
|
||||
'docstatus': 1
|
||||
}, true).as('doc');
|
||||
'docstatus': 1 // submit document
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
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.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-3').click(); // click outside
|
||||
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
cy.call('frappe.client.get_value', {
|
||||
doctype: doc.doctype,
|
||||
filters: {
|
||||
name: doc.name,
|
||||
},
|
||||
fieldname: 'enabled'
|
||||
}).then(r => {
|
||||
expect(r.message.enabled).to.equals(1);
|
||||
});
|
||||
|
||||
cy.call('frappe.client.get_value', {
|
||||
doctype: doctype_name,
|
||||
filters: {
|
||||
title: 'Doc 1',
|
||||
},
|
||||
fieldname: 'enabled'
|
||||
}).then(r => {
|
||||
expect(r.message.enabled).to.equals(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ context('Timeline Email', () => {
|
|||
cy.wait(700);
|
||||
});
|
||||
|
||||
it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
|
||||
it('Adding email and verifying timeline content for email attachment', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail');
|
||||
|
||||
|
|
@ -43,7 +43,9 @@ context('Timeline Email', () => {
|
|||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click();
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
|
||||
it('Deleting attachment and ToDo', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
|
|
@ -57,11 +59,11 @@ context('Timeline Email', () => {
|
|||
cy.wait(500);
|
||||
|
||||
//To check if the discard button functionality in email is working correctly
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
|
||||
cy.wait(500);
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.wait(500);
|
||||
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
|
||||
cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ context('Workspace 2.0', () => {
|
|||
// check if sidebar item is added in pubic section
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
|
|
@ -33,56 +33,54 @@ context('Workspace 2.0', () => {
|
|||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click();
|
||||
cy.get('.ce-block').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
cy.get(".ce-block:last").find('.ce-header').should('exist');
|
||||
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click();
|
||||
cy.get('.ce-block:last').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Text').click();
|
||||
cy.get(":focus").type('Paragraph text');
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
|
||||
});
|
||||
|
||||
it('Delete A Block', () => {
|
||||
cy.get(".ce-block:last").find('.delete-paragraph').click();
|
||||
cy.get(":focus").click();
|
||||
cy.get('.paragraph-control .setting-btn').click();
|
||||
cy.get('.paragraph-control .dropdown-item').contains('Delete').click();
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist');
|
||||
});
|
||||
|
||||
it('Shrink and Expand A Block', () => {
|
||||
cy.get(".ce-block:last").find('.tune-btn').click();
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-9');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-12');
|
||||
});
|
||||
cy.get(":focus").click();
|
||||
cy.get('.ce-block:last .setting-btn').click();
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-11');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-10');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-9');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-10');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-11');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-12');
|
||||
|
||||
it('Change Header Text Size', () => {
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="3"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h3').should('exist');
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="4"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h4').should('exist');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
});
|
||||
|
||||
it('Delete Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
.find('.sidebar-item-control .setting-btn').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -110,34 +110,6 @@ Cypress.Commands.add('get_doc', (doctype, name) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
||||
return cy
|
||||
.window()
|
||||
.its('frappe.csrf_token')
|
||||
.then(csrf_token => {
|
||||
return cy
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: `/api/resource/${doctype}`,
|
||||
body: args,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
},
|
||||
failOnStatusCode: !ignore_duplicate
|
||||
})
|
||||
.then(res => {
|
||||
let status_codes = [200];
|
||||
if (ignore_duplicate) {
|
||||
status_codes.push(409);
|
||||
}
|
||||
expect(res.status).to.be.oneOf(status_codes);
|
||||
return res.body;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('remove_doc', (doctype, name) => {
|
||||
return cy
|
||||
.window()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const cliui = require("cliui")();
|
|||
const chalk = require("chalk");
|
||||
const html_plugin = require("./frappe-html");
|
||||
const rtlcss = require('rtlcss');
|
||||
const postCssPlugin = require("esbuild-plugin-postcss2").default;
|
||||
const postCssPlugin = require("@frappe/esbuild-plugin-postcss2").default;
|
||||
const ignore_assets = require("./ignore-assets");
|
||||
const sass_options = require("./sass_options");
|
||||
const build_cleanup_plugin = require("./build-cleanup");
|
||||
|
|
@ -286,7 +286,7 @@ function get_watch_config() {
|
|||
notify_redis({ error });
|
||||
} else {
|
||||
let {
|
||||
assets_json,
|
||||
new_assets_json,
|
||||
prev_assets_json
|
||||
} = await write_assets_json(result.metafile);
|
||||
|
||||
|
|
@ -294,7 +294,7 @@ function get_watch_config() {
|
|||
if (prev_assets_json) {
|
||||
changed_files = get_rebuilt_assets(
|
||||
prev_assets_json,
|
||||
assets_json
|
||||
new_assets_json
|
||||
);
|
||||
|
||||
let timestamp = new Date().toLocaleTimeString();
|
||||
|
|
@ -384,6 +384,7 @@ let prev_assets_json;
|
|||
let curr_assets_json;
|
||||
|
||||
async function write_assets_json(metafile) {
|
||||
let rtl = false;
|
||||
prev_assets_json = curr_assets_json;
|
||||
let out = {};
|
||||
for (let output in metafile.outputs) {
|
||||
|
|
@ -392,13 +393,14 @@ async function write_assets_json(metafile) {
|
|||
if (info.entryPoint) {
|
||||
let key = path.basename(info.entryPoint);
|
||||
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
|
||||
rtl = true;
|
||||
key = `rtl_${key}`;
|
||||
}
|
||||
out[key] = asset_path;
|
||||
}
|
||||
}
|
||||
|
||||
let assets_json_path = path.resolve(assets_path, "assets.json");
|
||||
let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`);
|
||||
let assets_json;
|
||||
try {
|
||||
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
|
||||
|
|
@ -407,21 +409,21 @@ async function write_assets_json(metafile) {
|
|||
}
|
||||
assets_json = JSON.parse(assets_json);
|
||||
// update with new values
|
||||
assets_json = Object.assign({}, assets_json, out);
|
||||
curr_assets_json = assets_json;
|
||||
let new_assets_json = Object.assign({}, assets_json, out);
|
||||
curr_assets_json = new_assets_json;
|
||||
|
||||
await fs.promises.writeFile(
|
||||
assets_json_path,
|
||||
JSON.stringify(assets_json, null, 4)
|
||||
JSON.stringify(new_assets_json, null, 4)
|
||||
);
|
||||
await update_assets_json_in_cache(assets_json);
|
||||
await update_assets_json_in_cache();
|
||||
return {
|
||||
assets_json,
|
||||
new_assets_json,
|
||||
prev_assets_json
|
||||
};
|
||||
}
|
||||
|
||||
function update_assets_json_in_cache(assets_json) {
|
||||
function update_assets_json_in_cache() {
|
||||
// update assets_json cache in redis, so that it can be read directly by python
|
||||
return new Promise(resolve => {
|
||||
let client = get_redis_subscriber("redis_cache");
|
||||
|
|
@ -429,7 +431,7 @@ function update_assets_json_in_cache(assets_json) {
|
|||
client.on("error", _ => {
|
||||
log_warn("Cannot connect to redis_cache to update assets_json");
|
||||
});
|
||||
client.set("assets_json", JSON.stringify(assets_json), err => {
|
||||
client.del("assets_json", err => {
|
||||
client.unref();
|
||||
resolve();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ module.exports = {
|
|||
.then(content => {
|
||||
content = scrub_html_template(content);
|
||||
return {
|
||||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`
|
||||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`,
|
||||
watchFiles: [filepath]
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from frappe.query_builder import (
|
|||
patch_query_execute,
|
||||
patch_query_aggregation,
|
||||
)
|
||||
from frappe.utils.data import cstr
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
|
|
@ -102,7 +103,7 @@ def as_unicode(text, encoding='utf-8'):
|
|||
'''Convert to unicode if required'''
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
elif text==None:
|
||||
elif text is None:
|
||||
return ''
|
||||
elif isinstance(text, bytes):
|
||||
return str(text, encoding)
|
||||
|
|
@ -143,6 +144,8 @@ lang = local("lang")
|
|||
# This if block is never executed when running the code. It is only used for
|
||||
# telling static code analyzer where to find dynamically defined attributes.
|
||||
if typing.TYPE_CHECKING:
|
||||
from frappe.utils.redis_wrapper import RedisWrapper
|
||||
|
||||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
from frappe.database.postgres.database import PostgresDatabase
|
||||
from frappe.query_builder.builder import MariaDB, Postgres
|
||||
|
|
@ -150,6 +153,7 @@ if typing.TYPE_CHECKING:
|
|||
db: typing.Union[MariaDBDatabase, PostgresDatabase]
|
||||
qb: typing.Union[MariaDB, Postgres]
|
||||
|
||||
|
||||
# end: static analysis hack
|
||||
|
||||
def init(site, sites_path=None, new_site=False):
|
||||
|
|
@ -211,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
|
|||
local.cache = {}
|
||||
local.document_cache = {}
|
||||
local.meta_cache = {}
|
||||
local.autoincremented_status_map = {site: -1}
|
||||
local.form_dict = _dict()
|
||||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
|
|
@ -294,7 +299,7 @@ def get_conf(site=None):
|
|||
|
||||
class init_site:
|
||||
def __init__(self, site=None):
|
||||
'''If site==None, initialize it for empty site ('') to load common_site_config.json'''
|
||||
'''If site is None, initialize it for empty site ('') to load common_site_config.json'''
|
||||
self.site = site or ''
|
||||
|
||||
def __enter__(self):
|
||||
|
|
@ -311,9 +316,8 @@ def destroy():
|
|||
|
||||
release_local(local)
|
||||
|
||||
# memcache
|
||||
redis_server = None
|
||||
def cache():
|
||||
def cache() -> "RedisWrapper":
|
||||
"""Returns redis connection."""
|
||||
global redis_server
|
||||
if not redis_server:
|
||||
|
|
@ -356,7 +360,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
|
|||
response JSON and shown in a pop-up / modal.
|
||||
|
||||
:param msg: Message.
|
||||
:param title: [optional] Message title.
|
||||
:param title: [optional] Message title. Default: "Message".
|
||||
:param raise_exception: [optional] Raise given exception and show message.
|
||||
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
|
||||
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
|
||||
|
|
@ -393,8 +397,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
|
|||
if flags.print_messages and out.message:
|
||||
print(f"Message: {strip_html_tags(out.message)}")
|
||||
|
||||
if title:
|
||||
out.title = title
|
||||
out.title = title or _("Message", context="Default title of the message dialog")
|
||||
|
||||
if not indicator and raise_exception:
|
||||
indicator = 'red'
|
||||
|
|
@ -446,7 +449,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None,
|
|||
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list)
|
||||
|
||||
def emit_js(js, user=False, **kwargs):
|
||||
if user == False:
|
||||
if user is False:
|
||||
user = session.user
|
||||
publish_realtime('eval_js', js, user=user, **kwargs)
|
||||
|
||||
|
|
@ -849,8 +852,7 @@ def set_value(doctype, docname, fieldname, value=None):
|
|||
return frappe.client.set_value(doctype, docname, fieldname, value)
|
||||
|
||||
def get_cached_doc(*args, **kwargs):
|
||||
if args and len(args) > 1 and isinstance(args[1], str):
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
if key := can_cache_doc(args):
|
||||
# local cache
|
||||
doc = local.document_cache.get(key)
|
||||
if doc:
|
||||
|
|
@ -868,8 +870,24 @@ def get_cached_doc(*args, **kwargs):
|
|||
|
||||
return doc
|
||||
|
||||
def can_cache_doc(args):
|
||||
"""
|
||||
Determine if document should be cached based on get_doc params.
|
||||
Returns cache key if doc can be cached, None otherwise.
|
||||
"""
|
||||
|
||||
if not args:
|
||||
return
|
||||
|
||||
doctype = args[0]
|
||||
name = doctype if len(args) == 1 else args[1]
|
||||
|
||||
# Only cache if both doctype and name are strings
|
||||
if isinstance(doctype, str) and isinstance(name, str):
|
||||
return get_document_cache_key(doctype, name)
|
||||
|
||||
def get_document_cache_key(doctype, name):
|
||||
return '{0}::{1}'.format(doctype, name)
|
||||
return f'{doctype}::{name}'
|
||||
|
||||
def clear_document_cache(doctype, name):
|
||||
cache().hdel("last_modified", doctype)
|
||||
|
|
@ -910,8 +928,7 @@ def get_doc(*args, **kwargs):
|
|||
doc = frappe.model.document.get_doc(*args, **kwargs)
|
||||
|
||||
# set in cache
|
||||
if args and len(args) > 1:
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
if key := can_cache_doc(args):
|
||||
local.document_cache[key] = doc
|
||||
cache().hset('document_cache', key, doc.as_dict())
|
||||
|
||||
|
|
@ -961,8 +978,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
|
|||
|
||||
def delete_doc_if_exists(doctype, name, force=0):
|
||||
"""Delete document if exists."""
|
||||
if db.exists(doctype, name):
|
||||
delete_doc(doctype, name, force=force)
|
||||
delete_doc(doctype, name, force=force, ignore_missing=True)
|
||||
|
||||
def reload_doctype(doctype, force=False, reset_permissions=False):
|
||||
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files."""
|
||||
|
|
@ -1000,7 +1016,7 @@ def get_module(modulename):
|
|||
|
||||
def scrub(txt):
|
||||
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
|
||||
return txt.replace(' ', '_').replace('-', '_').lower()
|
||||
return cstr(txt).replace(' ', '_').replace('-', '_').lower()
|
||||
|
||||
def unscrub(txt):
|
||||
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
|
||||
|
|
@ -1235,9 +1251,10 @@ def get_newargs(fn, kwargs):
|
|||
if hasattr(fn, 'fnargs'):
|
||||
fnargs = fn.fnargs
|
||||
else:
|
||||
fnargs = inspect.getfullargspec(fn).args
|
||||
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
|
||||
varkw = inspect.getfullargspec(fn).varkw
|
||||
fullargspec = inspect.getfullargspec(fn)
|
||||
fnargs = fullargspec.args
|
||||
fnargs.extend(fullargspec.kwonlyargs)
|
||||
varkw = fullargspec.varkw
|
||||
|
||||
newargs = {}
|
||||
for a in kwargs:
|
||||
|
|
@ -1661,7 +1678,7 @@ def local_cache(namespace, key, generator, regenerate_if_none=False):
|
|||
if key not in local.cache[namespace]:
|
||||
local.cache[namespace][key] = generator()
|
||||
|
||||
elif local.cache[namespace][key]==None and regenerate_if_none:
|
||||
elif local.cache[namespace][key] is None and regenerate_if_none:
|
||||
# if key exists but the previous result was None
|
||||
local.cache[namespace][key] = generator()
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ def handle():
|
|||
"data": doc.save().as_dict()
|
||||
})
|
||||
|
||||
if doc.parenttype and doc.parent:
|
||||
# check for child table doctype
|
||||
if doc.get("parenttype"):
|
||||
frappe.get_doc(doc.parenttype, doc.parent).save()
|
||||
|
||||
frappe.db.commit()
|
||||
|
|
@ -158,7 +159,10 @@ def get_request_form_data():
|
|||
else:
|
||||
data = frappe.local.form_dict.data
|
||||
|
||||
return frappe.parse_json(data)
|
||||
try:
|
||||
return frappe.parse_json(data)
|
||||
except ValueError:
|
||||
return frappe.local.form_dict
|
||||
|
||||
|
||||
def validate_auth():
|
||||
|
|
@ -207,7 +211,6 @@ def validate_oauth(authorization_header):
|
|||
pass
|
||||
|
||||
|
||||
|
||||
def validate_auth_via_api_keys(authorization_header):
|
||||
"""
|
||||
Authenticate request using API keys and set session user
|
||||
|
|
|
|||
|
|
@ -192,12 +192,7 @@ def make_form_dict(request):
|
|||
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 args.items()
|
||||
})
|
||||
except IndexError:
|
||||
frappe.local.form_dict = frappe._dict(args)
|
||||
frappe.local.form_dict = frappe._dict(args)
|
||||
|
||||
if "_" in frappe.local.form_dict:
|
||||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
|
||||
|
|
@ -299,7 +294,6 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
|
|||
_sites_path = sites_path
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
patch_werkzeug_reloader()
|
||||
|
||||
if profile or os.environ.get('USE_PROFILER'):
|
||||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
|
||||
|
|
@ -330,23 +324,3 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
|
|||
use_debugger=not in_test_env,
|
||||
use_evalex=not in_test_env,
|
||||
threaded=not no_threading)
|
||||
|
||||
def patch_werkzeug_reloader():
|
||||
"""
|
||||
This function monkey patches Werkzeug reloader to ignore reloading files in
|
||||
the __pycache__ directory.
|
||||
|
||||
To be deprecated when upgrading to Werkzeug 2.
|
||||
"""
|
||||
|
||||
from werkzeug._reloader import WatchdogReloaderLoop
|
||||
|
||||
trigger_reload = WatchdogReloaderLoop.trigger_reload
|
||||
|
||||
def custom_trigger_reload(self, filename):
|
||||
if os.path.basename(os.path.dirname(filename)) == "__pycache__":
|
||||
return
|
||||
|
||||
return trigger_reload(self, filename)
|
||||
|
||||
WatchdogReloaderLoop.trigger_reload = custom_trigger_reload
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ class LoginManager:
|
|||
self.user_type = None
|
||||
|
||||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
|
||||
if self.login()==False: return
|
||||
if self.login() is False:
|
||||
return
|
||||
self.resume = False
|
||||
|
||||
# run login triggers
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
for todo in todos_to_close:
|
||||
_todo = frappe.get_doc("ToDo", todo)
|
||||
_todo.status = "Closed"
|
||||
_todo.save()
|
||||
_todo.save(ignore_permissions=True)
|
||||
break
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Event Streaming\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 14:53:24.980279",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -208,7 +208,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:02.839181",
|
||||
"modified": "2022-01-13 17:48:48.456763",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 26,
|
||||
"sequence_id": 26.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "ToDo",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ bootstrap client session
|
|||
import frappe
|
||||
import frappe.defaults
|
||||
import frappe.desk.desk_page
|
||||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
|
||||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.utils.change_log import get_versions
|
||||
from frappe.translate import get_lang_dict
|
||||
|
|
@ -15,10 +16,9 @@ from frappe.social.doctype.energy_point_settings.energy_point_settings import is
|
|||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
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, get_app_logo
|
||||
from frappe.geo.country_info import get_all
|
||||
from frappe.utils import get_time_zone
|
||||
from frappe.utils import get_time_zone, add_user_info
|
||||
|
||||
def get_bootinfo():
|
||||
"""build and return boot info"""
|
||||
|
|
@ -91,6 +91,7 @@ def get_bootinfo():
|
|||
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
|
||||
bootinfo.desk_settings = get_desk_settings()
|
||||
bootinfo.app_logo_url = get_app_logo()
|
||||
bootinfo.link_title_doctypes = get_link_title_doctypes()
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
|
@ -109,8 +110,8 @@ def load_conf_settings(bootinfo):
|
|||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.desk.desktop import get_wspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages')
|
||||
from frappe.desk.desktop import get_workspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages')
|
||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
|
|
@ -330,6 +331,16 @@ def get_country_codes(bootinfo):
|
|||
country_codes = get_all()
|
||||
bootinfo.country_codes = frappe._dict(country_codes)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_link_title_doctypes():
|
||||
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
|
||||
custom_dts = frappe.get_all(
|
||||
"Property Setter",
|
||||
{"property": "show_title_field_in_link", "value": "1"},
|
||||
["doc_type as name"],
|
||||
)
|
||||
return [d.name for d in dts + custom_dts if d]
|
||||
|
||||
def set_time_zone(bootinfo):
|
||||
bootinfo.time_zone = {
|
||||
"system": get_time_zone(),
|
||||
|
|
|
|||
149
frappe/build.py
149
frappe/build.py
|
|
@ -1,25 +1,21 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
from subprocess import getoutput
|
||||
from io import StringIO
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from distutils.spawn import find_executable
|
||||
|
||||
import frappe
|
||||
from frappe.utils.minify import JavascriptMinify
|
||||
from subprocess import getoutput
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
import psutil
|
||||
from urllib.parse import urlparse
|
||||
from semantic_version import Version
|
||||
from requests import head
|
||||
from requests.exceptions import HTTPError
|
||||
from semantic_version import Version
|
||||
|
||||
import frappe
|
||||
|
||||
timestamps = {}
|
||||
app_paths = None
|
||||
|
|
@ -32,6 +28,7 @@ class AssetsNotDownloadedError(Exception):
|
|||
class AssetsDontExistError(HTTPError):
|
||||
pass
|
||||
|
||||
|
||||
def download_file(url, prefix):
|
||||
from requests import get
|
||||
|
||||
|
|
@ -277,12 +274,14 @@ def check_node_executable():
|
|||
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
|
||||
click.echo()
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -296,6 +295,7 @@ def get_safe_max_old_space_size():
|
|||
|
||||
return safe_max_old_space_size
|
||||
|
||||
|
||||
def generate_assets_map():
|
||||
symlinks = {}
|
||||
|
||||
|
|
@ -344,7 +344,6 @@ def clear_broken_symlinks():
|
|||
os.remove(path)
|
||||
|
||||
|
||||
|
||||
def unstrip(message: str) -> str:
|
||||
"""Pads input string on the right side until the last available column in the terminal
|
||||
"""
|
||||
|
|
@ -397,94 +396,6 @@ def link_assets_dir(source, target, hard_link=False):
|
|||
symlink(source, target, overwrite=True)
|
||||
|
||||
|
||||
def build(no_compress=False, verbose=False):
|
||||
for target, sources in get_build_maps().items():
|
||||
pack(os.path.join(assets_path, target), sources, no_compress, verbose)
|
||||
|
||||
|
||||
def get_build_maps():
|
||||
"""get all build.jsons with absolute paths"""
|
||||
# framework js and css files
|
||||
|
||||
build_maps = {}
|
||||
for app_path in app_paths:
|
||||
path = os.path.join(app_path, "public", "build.json")
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
try:
|
||||
for target, sources in (json.loads(f.read() or "{}")).items():
|
||||
# update app path
|
||||
source_paths = []
|
||||
for source in sources:
|
||||
if isinstance(source, list):
|
||||
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
|
||||
else:
|
||||
s = os.path.join(app_path, source)
|
||||
source_paths.append(s)
|
||||
|
||||
build_maps[target] = source_paths
|
||||
except ValueError as e:
|
||||
print(path)
|
||||
print("JSON syntax error {0}".format(str(e)))
|
||||
return build_maps
|
||||
|
||||
|
||||
def pack(target, sources, no_compress, verbose):
|
||||
outtype, outtxt = target.split(".")[-1], ""
|
||||
jsm = JavascriptMinify()
|
||||
|
||||
for f in sources:
|
||||
suffix = None
|
||||
if ":" in f:
|
||||
f, suffix = f.split(":")
|
||||
if not os.path.exists(f) or os.path.isdir(f):
|
||||
print("did not find " + f)
|
||||
continue
|
||||
timestamps[f] = os.path.getmtime(f)
|
||||
try:
|
||||
with open(f, "r") as sourcefile:
|
||||
data = str(sourcefile.read(), "utf-8", errors="ignore")
|
||||
|
||||
extn = f.rsplit(".", 1)[1]
|
||||
|
||||
if (
|
||||
outtype == "js"
|
||||
and extn == "js"
|
||||
and (not no_compress)
|
||||
and suffix != "concat"
|
||||
and (".min." not in f)
|
||||
):
|
||||
tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO()
|
||||
jsm.minify(tmpin, tmpout)
|
||||
minified = tmpout.getvalue()
|
||||
if minified:
|
||||
outtxt += str(minified or "", "utf-8").strip("\n") + ";"
|
||||
|
||||
if verbose:
|
||||
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
|
||||
elif outtype == "js" and extn == "html":
|
||||
# add to frappe.templates
|
||||
outtxt += html_to_js_template(f, data)
|
||||
else:
|
||||
outtxt += "\n/*\n *\t%s\n */" % f
|
||||
outtxt += "\n" + data + "\n"
|
||||
|
||||
except Exception:
|
||||
print("--Error in:" + f + "--")
|
||||
print(frappe.get_traceback())
|
||||
|
||||
with open(target, "w") as f:
|
||||
f.write(outtxt.encode("utf-8"))
|
||||
|
||||
print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024))))
|
||||
|
||||
|
||||
def html_to_js_template(path, content):
|
||||
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
|
||||
return """frappe.templates["{key}"] = '{content}';\n""".format(
|
||||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
|
||||
|
||||
|
||||
def scrub_html_template(content):
|
||||
"""Returns HTML content with removed whitespace and comments"""
|
||||
# remove whitespace to a single space
|
||||
|
|
@ -496,37 +407,7 @@ def scrub_html_template(content):
|
|||
return content.replace("'", "\'")
|
||||
|
||||
|
||||
def files_dirty():
|
||||
for target, sources in get_build_maps().items():
|
||||
for f in sources:
|
||||
if ":" in f:
|
||||
f, suffix = f.split(":")
|
||||
if not os.path.exists(f) or os.path.isdir(f):
|
||||
continue
|
||||
if os.path.getmtime(f) != timestamps.get(f):
|
||||
print(f + " dirty")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def compile_less():
|
||||
if not find_executable("lessc"):
|
||||
return
|
||||
|
||||
for path in app_paths:
|
||||
less_path = os.path.join(path, "public", "less")
|
||||
if os.path.exists(less_path):
|
||||
for fname in os.listdir(less_path):
|
||||
if fname.endswith(".less") and fname != "variables.less":
|
||||
fpath = os.path.join(less_path, fname)
|
||||
mtime = os.path.getmtime(fpath)
|
||||
if fpath in timestamps and mtime == timestamps[fpath]:
|
||||
continue
|
||||
|
||||
timestamps[fpath] = mtime
|
||||
|
||||
print("compiling {0}".format(fpath))
|
||||
|
||||
css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
|
||||
os.system("lessc {0} > {1}".format(fpath, css_path))
|
||||
def html_to_js_template(path, content):
|
||||
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
|
||||
return """frappe.templates["{key}"] = '{content}';\n""".format(
|
||||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ def build_table_count_cache():
|
|||
data = (
|
||||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
|
||||
).run(as_dict=True)
|
||||
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
|
||||
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data}
|
||||
_cache.set_value("information_schema:counts", counts)
|
||||
|
||||
return counts
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
if not filters:
|
||||
filters = None
|
||||
|
||||
|
||||
if frappe.get_meta(doctype).issingle:
|
||||
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
|
||||
else:
|
||||
|
|
@ -129,7 +128,7 @@ def set_value(doctype, name, fieldname, value=None):
|
|||
:param fieldname: fieldname string or JSON / dict with key value pair
|
||||
:param value: value if fieldname is JSON / dict'''
|
||||
|
||||
if fieldname!="idx" and fieldname in frappe.model.default_fields:
|
||||
if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields):
|
||||
frappe.throw(_("Cannot edit standard fields"))
|
||||
|
||||
if not value:
|
||||
|
|
@ -142,14 +141,15 @@ def set_value(doctype, name, fieldname, value=None):
|
|||
else:
|
||||
values = {fieldname: value}
|
||||
|
||||
doc = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True)
|
||||
if doc and doc.parent and doc.parenttype:
|
||||
# check for child table doctype
|
||||
if not frappe.get_meta(doctype).istable:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
doc.update(values)
|
||||
else:
|
||||
doc = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True)
|
||||
doc = frappe.get_doc(doc.parenttype, doc.parent)
|
||||
child = doc.getone({"doctype": doctype, "name": name})
|
||||
child.update(values)
|
||||
else:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
doc.update(values)
|
||||
|
||||
doc.save()
|
||||
|
||||
|
|
@ -163,10 +163,10 @@ def insert(doc=None):
|
|||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if doc.get("parent") and doc.get("parenttype"):
|
||||
if doc.get("parenttype"):
|
||||
# inserting a child record
|
||||
parent = frappe.get_doc(doc.get("parenttype"), doc.get("parent"))
|
||||
parent.append(doc.get("parentfield"), doc)
|
||||
parent = frappe.get_doc(doc.parenttype, doc.parent)
|
||||
parent.append(doc.parentfield, doc)
|
||||
parent.save()
|
||||
return parent.as_dict()
|
||||
else:
|
||||
|
|
@ -187,10 +187,10 @@ def insert_many(docs=None):
|
|||
frappe.throw(_('Only 200 inserts allowed in one request'))
|
||||
|
||||
for doc in docs:
|
||||
if doc.get("parent") and doc.get("parenttype"):
|
||||
if doc.get("parenttype"):
|
||||
# inserting a child record
|
||||
parent = frappe.get_doc(doc.get("parenttype"), doc.get("parent"))
|
||||
parent.append(doc.get("parentfield"), doc)
|
||||
parent = frappe.get_doc(doc.parenttype, doc.parent)
|
||||
parent.append(doc.parentfield, doc)
|
||||
parent.save()
|
||||
out.append(parent.name)
|
||||
else:
|
||||
|
|
|
|||
95
frappe/commands/site.py
Executable file → Normal file
95
frappe/commands/site.py
Executable file → Normal file
|
|
@ -1,7 +1,7 @@
|
|||
# imports - standard imports
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
|
|
@ -19,36 +19,38 @@ from frappe.exceptions import SiteNotSpecifiedError
|
|||
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
|
||||
@click.option('--db-host', help='Database Host')
|
||||
@click.option('--db-port', type=int, help='Database Port')
|
||||
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
|
||||
@click.option('--mariadb-root-password', help='Root password for MariaDB')
|
||||
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
|
||||
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
|
||||
@click.option('--no-mariadb-socket', is_flag=True, default=False, help='Set MariaDB host to % and use TCP/IP Socket instead of using the UNIX Socket')
|
||||
@click.option('--admin-password', help='Administrator password for new site', default=None)
|
||||
@click.option('--verbose', is_flag=True, default=False, help='Verbose')
|
||||
@click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False)
|
||||
@click.option('--source_sql', help='Initiate database with a SQL file')
|
||||
@click.option('--install-app', multiple=True, help='Install app after installation')
|
||||
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
|
||||
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):
|
||||
@click.option('--set-default', is_flag=True, default=False, help='Set the new site as default site')
|
||||
def new_site(site, db_root_username=None, db_root_password=None, admin_password=None,
|
||||
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,
|
||||
set_default=False):
|
||||
"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,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
|
||||
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
|
||||
db_port=db_port, new_site=True)
|
||||
_new_site(db_name, site, db_root_username=db_root_username,
|
||||
db_root_password=db_root_password, admin_password=admin_password,
|
||||
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
|
||||
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
|
||||
db_port=db_port, new_site=True)
|
||||
|
||||
if len(frappe.utils.get_sites()) == 1:
|
||||
if set_default:
|
||||
use(site)
|
||||
|
||||
|
||||
@click.command('restore')
|
||||
@click.argument('sql-file-path')
|
||||
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
|
||||
@click.option('--mariadb-root-password', help='Root password for MariaDB')
|
||||
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
|
||||
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
|
||||
@click.option('--db-name', help='Database name for site in case it is a new one')
|
||||
@click.option('--admin-password', help='Administrator password for new site')
|
||||
@click.option('--install-app', multiple=True, help='Install app after installation')
|
||||
|
|
@ -57,17 +59,17 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
|
||||
@click.option('--encryption-key', help='Backup encryption key')
|
||||
@pass_context
|
||||
def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None,
|
||||
def restore(context, sql_file_path, encryption_key=None, db_root_username=None, db_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,
|
||||
extract_sql_from_archive,
|
||||
is_downgrade,
|
||||
is_partial,
|
||||
validate_database_sql
|
||||
validate_database_sql,
|
||||
)
|
||||
from frappe.utils.backups import Backup
|
||||
if not os.path.exists(sql_file_path):
|
||||
|
|
@ -150,8 +152,8 @@ def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=N
|
|||
|
||||
|
||||
try:
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
_new_site(frappe.conf.db_name, site, db_root_username=db_root_username,
|
||||
db_root_password=db_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
|
|
@ -205,7 +207,7 @@ def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=N
|
|||
@click.option('--encryption-key', help='Backup encryption key')
|
||||
@pass_context
|
||||
def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
||||
from frappe.installer import partial_restore, extract_sql_from_archive
|
||||
from frappe.installer import extract_sql_from_archive, partial_restore
|
||||
from frappe.utils.backups import Backup
|
||||
|
||||
if not os.path.exists(sql_file_path):
|
||||
|
|
@ -290,16 +292,16 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
|||
|
||||
@click.command('reinstall')
|
||||
@click.option('--admin-password', help='Administrator Password for reinstalled site')
|
||||
@click.option('--mariadb-root-username', help='Root username for MariaDB')
|
||||
@click.option('--mariadb-root-password', help='Root password for MariaDB')
|
||||
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
|
||||
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
|
||||
@click.option('--yes', is_flag=True, default=False, help='Pass --yes to skip confirmation')
|
||||
@pass_context
|
||||
def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False):
|
||||
def reinstall(context, admin_password=None, db_root_username=None, db_root_password=None, yes=False):
|
||||
"Reinstall site ie. wipe all data and start over"
|
||||
site = get_site(context)
|
||||
_reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose)
|
||||
_reinstall(site, admin_password, db_root_username, db_root_password, yes, verbose=context.verbose)
|
||||
|
||||
def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False):
|
||||
def _reinstall(site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False):
|
||||
from frappe.installer import _new_site
|
||||
|
||||
if not yes:
|
||||
|
|
@ -319,7 +321,7 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
|
|||
|
||||
frappe.init(site=site)
|
||||
_new_site(frappe.conf.db_name, site, verbose=verbose, force=True, reinstall=True, install_apps=installed,
|
||||
mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password,
|
||||
db_root_username=db_root_username, db_root_password=db_root_password,
|
||||
admin_password=admin_password)
|
||||
|
||||
@click.command('install-app')
|
||||
|
|
@ -447,21 +449,17 @@ def disable_user(context, email):
|
|||
@pass_context
|
||||
def migrate(context, skip_failing=False, skip_search_index=False):
|
||||
"Run patches, sync schema and rebuild files/translations"
|
||||
from frappe.migrate import migrate
|
||||
from frappe.migrate import SiteMigration
|
||||
|
||||
for site in context.sites:
|
||||
click.secho(f"Migrating {site}", fg="green")
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
try:
|
||||
migrate(
|
||||
context.verbose,
|
||||
SiteMigration(
|
||||
skip_failing=skip_failing,
|
||||
skip_search_index=skip_search_index
|
||||
)
|
||||
skip_search_index=skip_search_index,
|
||||
).run(site=site)
|
||||
finally:
|
||||
print()
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
|
@ -547,7 +545,7 @@ def _use(site, sites_path='.'):
|
|||
|
||||
def use(site, sites_path='.'):
|
||||
if os.path.exists(os.path.join(sites_path, site)):
|
||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
|
||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
|
||||
sitefile.write(site)
|
||||
print("Current Site set to {}".format(site))
|
||||
else:
|
||||
|
|
@ -660,16 +658,16 @@ def uninstall(context, app, dry_run, yes, no_backup, force):
|
|||
|
||||
@click.command('drop-site')
|
||||
@click.argument('site')
|
||||
@click.option('--root-login', default='root')
|
||||
@click.option('--root-password')
|
||||
@click.option('--db-root-username', '--mariadb-root-username', '--root-login', help='Root username for MariaDB or PostgreSQL, Default is "root"')
|
||||
@click.option('--db-root-password', '--mariadb-root-password', '--root-password', help='Root password for MariaDB or PostgreSQL')
|
||||
@click.option('--archived-sites-path')
|
||||
@click.option('--no-backup', is_flag=True, default=False)
|
||||
@click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False)
|
||||
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
|
||||
_drop_site(site, root_login, root_password, archived_sites_path, force, no_backup)
|
||||
def drop_site(site, db_root_username='root', db_root_password=None, archived_sites_path=None, force=False, no_backup=False):
|
||||
_drop_site(site, db_root_username, db_root_password, archived_sites_path, force, no_backup)
|
||||
|
||||
|
||||
def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
|
||||
def _drop_site(site, db_root_username=None, db_root_password=None, archived_sites_path=None, force=False, no_backup=False):
|
||||
"Remove site from database and filesystem"
|
||||
from frappe.database import drop_user_and_database
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
|
|
@ -679,7 +677,9 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
|||
|
||||
try:
|
||||
if not no_backup:
|
||||
scheduled_backup(ignore_files=False, force=True)
|
||||
click.secho(f"Taking backup of {site}", fg="green")
|
||||
odb = scheduled_backup(ignore_files=False, force=True, verbose=True)
|
||||
odb.print_summary()
|
||||
except Exception as err:
|
||||
if force:
|
||||
pass
|
||||
|
|
@ -694,7 +694,8 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
|||
click.echo("\n".join(messages))
|
||||
sys.exit(1)
|
||||
|
||||
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
|
||||
click.secho("Dropping site database and user", fg="green")
|
||||
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)
|
||||
|
||||
archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')
|
||||
|
||||
|
|
@ -753,6 +754,7 @@ def set_admin_password(context, admin_password=None, logout_all_sessions=False):
|
|||
|
||||
def set_user_password(site, user, password, logout_all_sessions=False):
|
||||
import getpass
|
||||
|
||||
from frappe.utils.password import update_password
|
||||
|
||||
try:
|
||||
|
|
@ -883,15 +885,16 @@ def stop_recording(context):
|
|||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('ngrok')
|
||||
@click.option('--bind-tls', is_flag=True, default=False, help='Returns a reference to the https tunnel.')
|
||||
@pass_context
|
||||
def start_ngrok(context):
|
||||
def start_ngrok(context, bind_tls):
|
||||
from pyngrok import ngrok
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
port = frappe.conf.http_port or frappe.conf.webserver_port
|
||||
tunnel = ngrok.connect(addr=str(port), host_header=site)
|
||||
tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls)
|
||||
print(f'Public URL: {tunnel.public_url}')
|
||||
print('Inspect logs at http://localhost:4040')
|
||||
|
||||
|
|
@ -952,7 +955,7 @@ def trim_database(context, dry_run, format, no_backup):
|
|||
doctype_tables = frappe.get_all("DocType", pluck="name")
|
||||
|
||||
for x in database_tables:
|
||||
doctype = x.lstrip("tab")
|
||||
doctype = x.replace("tab", "", 1)
|
||||
if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES):
|
||||
TABLES_TO_DROP.append(x)
|
||||
|
||||
|
|
@ -966,7 +969,7 @@ def trim_database(context, dry_run, format, no_backup):
|
|||
|
||||
odb = scheduled_backup(
|
||||
ignore_conf=False,
|
||||
include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP),
|
||||
include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP),
|
||||
ignore_files=True,
|
||||
force=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -623,6 +623,7 @@ def transform_database(context, table, engine, row_format, failfast):
|
|||
@click.command('run-tests')
|
||||
@click.option('--app', help="For App")
|
||||
@click.option('--doctype', help="For DocType")
|
||||
@click.option('--case', help="Select particular TestCase")
|
||||
@click.option('--doctype-list-path', help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt")
|
||||
@click.option('--test', multiple=True, help="Specific test")
|
||||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
|
||||
|
|
@ -636,9 +637,10 @@ def transform_database(context, table, engine, row_format, failfast):
|
|||
@pass_context
|
||||
def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False,
|
||||
coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
|
||||
skip_test_records=False, skip_before_tests=False, failfast=False):
|
||||
skip_test_records=False, skip_before_tests=False, failfast=False, case=None):
|
||||
|
||||
with CodeCoverage(coverage, app):
|
||||
import frappe
|
||||
import frappe.test_runner
|
||||
tests = test
|
||||
site = get_site(context)
|
||||
|
|
@ -658,7 +660,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
|
||||
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
|
||||
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast, case=case)
|
||||
|
||||
if len(ret.failures) == 0 and len(ret.errors) == 0:
|
||||
ret = 0
|
||||
|
|
@ -741,8 +743,9 @@ def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=Fals
|
|||
@click.option('--profile', is_flag=True, default=False)
|
||||
@click.option('--noreload', "no_reload", is_flag=True, default=False)
|
||||
@click.option('--nothreading', "no_threading", is_flag=True, default=False)
|
||||
@click.option('--with-coverage', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None):
|
||||
def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None, with_coverage=False):
|
||||
"Start development web server"
|
||||
import frappe.app
|
||||
|
||||
|
|
@ -750,8 +753,12 @@ def serve(context, port=None, profile=False, no_reload=False, no_threading=False
|
|||
site = None
|
||||
else:
|
||||
site = context.sites[0]
|
||||
|
||||
frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
|
||||
with CodeCoverage(with_coverage, 'frappe'):
|
||||
if with_coverage:
|
||||
# unable to track coverage with threading enabled
|
||||
no_threading = True
|
||||
no_reload = True
|
||||
frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
|
||||
|
||||
|
||||
@click.command('request')
|
||||
|
|
|
|||
|
|
@ -70,6 +70,19 @@ class TestComment(unittest.TestCase):
|
|||
reference_name = test_blog.name
|
||||
))), 0)
|
||||
|
||||
# test for filtering html and css injection elements
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
frappe.form_dict.comment = '<script>alert(1)</script>Comment'
|
||||
frappe.form_dict.comment_by = 'hacker'
|
||||
|
||||
add_comment()
|
||||
|
||||
self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
reference_name = test_blog.name
|
||||
))[0]['content'], 'Comment')
|
||||
|
||||
test_blog.delete()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
from collections import Counter
|
||||
from typing import List
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -17,6 +18,7 @@ from urllib.parse import unquote
|
|||
from frappe.utils.user import is_system_user
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
|
||||
from parse import compile
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
|
|
@ -113,6 +115,44 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
frappe.publish_realtime('new_message', self.as_dict(),
|
||||
user=self.reference_name, after_commit=True)
|
||||
|
||||
def set_signature_in_email_content(self):
|
||||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email
|
||||
"""
|
||||
if not self.content:
|
||||
return
|
||||
|
||||
quill_parser = compile('<div class="ql-editor read-mode">{}</div>')
|
||||
email_body = quill_parser.parse(self.content)
|
||||
|
||||
if not email_body:
|
||||
return
|
||||
|
||||
email_body = email_body[0]
|
||||
|
||||
user_email_signature = frappe.db.get_value(
|
||||
"User",
|
||||
self.sender,
|
||||
"email_signature",
|
||||
) if self.sender else None
|
||||
|
||||
signature = user_email_signature or frappe.db.get_value(
|
||||
"Email Account",
|
||||
{"default_outgoing": 1, "add_signature": 1},
|
||||
"signature",
|
||||
)
|
||||
|
||||
if not signature:
|
||||
return
|
||||
|
||||
_signature = quill_parser.parse(signature)[0] if "ql-editor" in signature else None
|
||||
|
||||
if (_signature or signature) not in self.content:
|
||||
self.content = f'{self.content}</p><br><p class="signature">{signature}'
|
||||
|
||||
def before_save(self):
|
||||
if not self.flags.skip_add_signature:
|
||||
self.set_signature_in_email_content()
|
||||
|
||||
def on_update(self):
|
||||
# add to _comment property of the doctype, so it shows up in
|
||||
# comments count for the list view
|
||||
|
|
@ -367,15 +407,8 @@ def get_permission_query_conditions_for_communication(user):
|
|||
return """`tabCommunication`.email_account in ({email_accounts})"""\
|
||||
.format(email_accounts=','.join(email_accounts))
|
||||
|
||||
def get_contacts(email_strings, auto_create_contact=False):
|
||||
email_addrs = []
|
||||
|
||||
for email_string in email_strings:
|
||||
if email_string:
|
||||
result = getaddresses([email_string])
|
||||
for email in result:
|
||||
email_addrs.append(email[1])
|
||||
|
||||
def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
|
||||
email_addrs = get_emails(email_strings)
|
||||
contacts = []
|
||||
for email in email_addrs:
|
||||
email = get_email_without_link(email)
|
||||
|
|
@ -404,6 +437,17 @@ def get_contacts(email_strings, auto_create_contact=False):
|
|||
|
||||
return contacts
|
||||
|
||||
def get_emails(email_strings: List[str]) -> List[str]:
|
||||
email_addrs = []
|
||||
|
||||
for email_string in email_strings:
|
||||
if email_string:
|
||||
result = getaddresses([email_string])
|
||||
for email in result:
|
||||
email_addrs.append(email[1])
|
||||
|
||||
return email_addrs
|
||||
|
||||
def add_contact_links_to_communication(communication, contact_name):
|
||||
contact_links = frappe.get_all("Dynamic Link", filters={
|
||||
"parenttype": "Contact",
|
||||
|
|
@ -449,8 +493,12 @@ def get_email_without_link(email):
|
|||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
|
||||
return email
|
||||
|
||||
email_id = email.split("@")[0].split("+")[0]
|
||||
email_host = email.split("@")[1]
|
||||
try:
|
||||
_email = email.split("@")
|
||||
email_id = _email[0].split("+")[0]
|
||||
email_host = _email[1]
|
||||
except IndexError:
|
||||
return email
|
||||
|
||||
return "{0}@{1}".format(email_id, email_host)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,51 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
import json
|
||||
from email.utils import formataddr
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import (get_url, get_formatted_email, cint, list_to_str,
|
||||
validate_email_address, split_emails, parse_addr, get_datetime)
|
||||
from frappe.email.email_body import get_message_id
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
import frappe
|
||||
import frappe.email.smtp
|
||||
import time
|
||||
from frappe import _
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.email.email_body import get_message_id
|
||||
from frappe.utils import (cint, get_datetime, get_formatted_email,
|
||||
list_to_str, split_emails, validate_email_address)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.communication.communication import Communication
|
||||
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
|
||||
Unable to send mail because of a missing email account.
|
||||
Please setup default Email Account from Setup > Email > Email Account
|
||||
""")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
|
||||
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
|
||||
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None,
|
||||
flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None,
|
||||
ignore_permissions=False):
|
||||
"""Make a new communication.
|
||||
def make(
|
||||
doctype=None,
|
||||
name=None,
|
||||
content=None,
|
||||
subject=None,
|
||||
sent_or_received="Sent",
|
||||
sender=None,
|
||||
sender_full_name=None,
|
||||
recipients=None,
|
||||
communication_medium="Email",
|
||||
send_email=False,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
attachments="[]",
|
||||
send_me_a_copy=False,
|
||||
cc=None,
|
||||
bcc=None,
|
||||
read_receipt=None,
|
||||
print_letterhead=True,
|
||||
email_template=None,
|
||||
communication_type=None,
|
||||
**kwargs,
|
||||
) -> Dict[str, str]:
|
||||
"""Make a new communication. Checks for email permissions for specified Document.
|
||||
|
||||
:param doctype: Reference DocType.
|
||||
:param name: Reference Document name.
|
||||
|
|
@ -41,22 +62,76 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
:param send_me_a_copy: Send a copy to the sender (default **False**).
|
||||
:param email_template: Template which is used to compose mail .
|
||||
"""
|
||||
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
|
||||
send_me_a_copy = cint(send_me_a_copy)
|
||||
if kwargs:
|
||||
from frappe.utils.commands import warn
|
||||
warn(
|
||||
f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
|
||||
"are deprecated or unsupported",
|
||||
category=DeprecationWarning
|
||||
)
|
||||
|
||||
if not ignore_permissions:
|
||||
if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not (flags or {}).get('ignore_doctype_permissions'):
|
||||
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
|
||||
doctype=doctype, name=name))
|
||||
if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name):
|
||||
raise frappe.PermissionError(
|
||||
f"You are not allowed to send emails related to: {doctype} {name}"
|
||||
)
|
||||
|
||||
if not sender:
|
||||
sender = get_formatted_email(frappe.session.user)
|
||||
return _make(
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
content=content,
|
||||
subject=subject,
|
||||
sent_or_received=sent_or_received,
|
||||
sender=sender,
|
||||
sender_full_name=sender_full_name,
|
||||
recipients=recipients,
|
||||
communication_medium=communication_medium,
|
||||
send_email=send_email,
|
||||
print_html=print_html,
|
||||
print_format=print_format,
|
||||
attachments=attachments,
|
||||
send_me_a_copy=cint(send_me_a_copy),
|
||||
cc=cc,
|
||||
bcc=bcc,
|
||||
read_receipt=read_receipt,
|
||||
print_letterhead=print_letterhead,
|
||||
email_template=email_template,
|
||||
communication_type=communication_type,
|
||||
add_signature=False,
|
||||
)
|
||||
|
||||
|
||||
def _make(
|
||||
doctype=None,
|
||||
name=None,
|
||||
content=None,
|
||||
subject=None,
|
||||
sent_or_received="Sent",
|
||||
sender=None,
|
||||
sender_full_name=None,
|
||||
recipients=None,
|
||||
communication_medium="Email",
|
||||
send_email=False,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
attachments="[]",
|
||||
send_me_a_copy=False,
|
||||
cc=None,
|
||||
bcc=None,
|
||||
read_receipt=None,
|
||||
print_letterhead=True,
|
||||
email_template=None,
|
||||
communication_type=None,
|
||||
add_signature=True,
|
||||
) -> Dict[str, str]:
|
||||
"""Internal method to make a new communication that ignores Permission checks.
|
||||
"""
|
||||
|
||||
sender = sender or get_formatted_email(frappe.session.user)
|
||||
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
|
||||
cc = list_to_str(cc) if isinstance(cc, list) else cc
|
||||
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
|
||||
|
||||
comm = frappe.get_doc({
|
||||
comm: "Communication" = frappe.get_doc({
|
||||
"doctype":"Communication",
|
||||
"subject": subject,
|
||||
"content": content,
|
||||
|
|
@ -73,32 +148,36 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
"message_id":get_message_id().strip(" <>"),
|
||||
"read_receipt":read_receipt,
|
||||
"has_attachment": 1 if attachments else 0,
|
||||
"communication_type": communication_type
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
if isinstance(attachments, str):
|
||||
attachments = json.loads(attachments)
|
||||
"communication_type": communication_type,
|
||||
})
|
||||
comm.flags.skip_add_signature = not add_signature
|
||||
comm.insert(ignore_permissions=True)
|
||||
|
||||
# if not committed, delayed task doesn't find the communication
|
||||
if attachments:
|
||||
if isinstance(attachments, str):
|
||||
attachments = json.loads(attachments)
|
||||
add_attachments(comm.name, attachments)
|
||||
|
||||
if cint(send_email):
|
||||
if not comm.get_outgoing_email_account():
|
||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
|
||||
frappe.throw(
|
||||
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError
|
||||
)
|
||||
|
||||
comm.send_email(print_html=print_html, print_format=print_format,
|
||||
send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead)
|
||||
comm.send_email(
|
||||
print_html=print_html,
|
||||
print_format=print_format,
|
||||
send_me_a_copy=send_me_a_copy,
|
||||
print_letterhead=print_letterhead,
|
||||
)
|
||||
|
||||
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
|
||||
return {
|
||||
"name": comm.name,
|
||||
"emails_not_sent_to": ", ".join(emails_not_sent_to or [])
|
||||
}
|
||||
|
||||
def validate_email(doc):
|
||||
return {"name": comm.name, "emails_not_sent_to": ", ".join(emails_not_sent_to)}
|
||||
|
||||
|
||||
def validate_email(doc: "Communication") -> None:
|
||||
"""Validate Email Addresses of Recipients and CC"""
|
||||
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive:
|
||||
return
|
||||
|
|
@ -114,8 +193,6 @@ def validate_email(doc):
|
|||
for email in split_emails(doc.bcc):
|
||||
validate_email_address(email, throw=True)
|
||||
|
||||
# validate sender
|
||||
|
||||
def set_incoming_outgoing_accounts(doc):
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
incoming_email_account = EmailAccount.find_incoming(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from typing import List
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.utils import get_parent_doc
|
||||
|
|
@ -194,14 +195,18 @@ class CommunicationEmailMixin:
|
|||
return _("Leave this conversation")
|
||||
return ''
|
||||
|
||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False):
|
||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List:
|
||||
"""List of mail id's excluded while sending mail.
|
||||
"""
|
||||
all_ids = self.get_all_email_addresses(exclude_displayname=True)
|
||||
final_ids = self.mail_recipients(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
|
||||
self.mail_bcc(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
|
||||
self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender)
|
||||
return set(all_ids) - set(final_ids)
|
||||
|
||||
final_ids = (
|
||||
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender)
|
||||
)
|
||||
|
||||
return list(set(all_ids) - set(final_ids))
|
||||
|
||||
def get_assignees(self):
|
||||
"""Get owners of the reference document.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import unittest
|
|||
from urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.communication.communication import get_emails
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
|
|
@ -201,6 +202,19 @@ class TestCommunication(unittest.TestCase):
|
|||
|
||||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
||||
def test_parse_emails(self):
|
||||
emails = get_emails(
|
||||
[
|
||||
'comm_recipient+DocType+DocName@example.com',
|
||||
'"First, LastName" <first.lastname@email.com>',
|
||||
'test@user.com'
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(emails[0], "comm_recipient+DocType+DocName@example.com")
|
||||
self.assertEqual(emails[1], "first.lastname@email.com")
|
||||
self.assertEqual(emails[2], "test@user.com")
|
||||
|
||||
class TestCommunicationEmailMixin(unittest.TestCase):
|
||||
def new_communication(self, recipients=None, cc=None, bcc=None):
|
||||
recipients = ', '.join(recipients or [])
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ class DataExporter:
|
|||
d = doc.copy()
|
||||
meta = frappe.get_meta(dt)
|
||||
if self.all_doctypes:
|
||||
d.name = '"'+ d.name+'"'
|
||||
d.name = f'"{d.name}"'
|
||||
|
||||
if len(rows) < rowidx + 1:
|
||||
rows.append([""] * (len(self.columns) + 1))
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ frappe.ui.form.on('Data Import', {
|
|||
}
|
||||
frm.dashboard.show_progress(__('Import Progress'), percent, message);
|
||||
frm.page.set_indicator(__('In Progress'), 'orange');
|
||||
frm.trigger('update_primary_action');
|
||||
|
||||
// hide progress when complete
|
||||
if (data.current === data.total) {
|
||||
|
|
@ -80,7 +81,10 @@ frappe.ui.form.on('Data Import', {
|
|||
frm.trigger('show_import_log');
|
||||
frm.trigger('show_import_warnings');
|
||||
frm.trigger('toggle_submit_after_import');
|
||||
frm.trigger('show_import_status');
|
||||
|
||||
if (frm.doc.status != 'Pending')
|
||||
frm.trigger('show_import_status');
|
||||
|
||||
frm.trigger('show_report_error_button');
|
||||
|
||||
if (frm.doc.status === 'Partial Success') {
|
||||
|
|
@ -128,40 +132,49 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
show_import_status(frm) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || '[]');
|
||||
let successful_records = import_log.filter(log => log.success);
|
||||
let failed_records = import_log.filter(log => !log.success);
|
||||
if (successful_records.length === 0) return;
|
||||
frappe.call({
|
||||
'method': 'frappe.core.doctype.data_import.data_import.get_import_status',
|
||||
'args': {
|
||||
'data_import_name': frm.doc.name
|
||||
},
|
||||
'callback': function(r) {
|
||||
let successful_records = cint(r.message.success);
|
||||
let failed_records = cint(r.message.failed);
|
||||
let total_records = cint(r.message.total_records);
|
||||
|
||||
let message;
|
||||
if (failed_records.length === 0) {
|
||||
let message_args = [successful_records.length];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully imported {0} records.', message_args)
|
||||
: __('Successfully imported {0} record.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully updated {0} records.', message_args)
|
||||
: __('Successfully updated {0} record.', message_args);
|
||||
if (!total_records) return;
|
||||
|
||||
let message;
|
||||
if (failed_records === 0) {
|
||||
let message_args = [successful_records];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully imported {0} records.', message_args)
|
||||
: __('Successfully imported {0} record.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully updated {0} records.', message_args)
|
||||
: __('Successfully updated {0} record.', message_args);
|
||||
}
|
||||
} else {
|
||||
let message_args = [successful_records, total_records];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
}
|
||||
}
|
||||
frm.dashboard.set_headline(message);
|
||||
}
|
||||
} else {
|
||||
let message_args = [successful_records.length, import_log.length];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
}
|
||||
}
|
||||
frm.dashboard.set_headline(message);
|
||||
});
|
||||
},
|
||||
|
||||
show_report_error_button(frm) {
|
||||
|
|
@ -275,7 +288,7 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
show_import_preview(frm, preview_data) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || '[]');
|
||||
let import_log = preview_data.import_log;
|
||||
|
||||
if (
|
||||
frm.import_preview &&
|
||||
|
|
@ -316,6 +329,15 @@ frappe.ui.form.on('Data Import', {
|
|||
);
|
||||
},
|
||||
|
||||
export_import_log(frm) {
|
||||
open_url_post(
|
||||
'/api/method/frappe.core.doctype.data_import.data_import.download_import_log',
|
||||
{
|
||||
data_import_name: frm.doc.name
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
show_import_warnings(frm, preview_data) {
|
||||
let columns = preview_data.columns;
|
||||
let warnings = JSON.parse(frm.doc.template_warnings || '[]');
|
||||
|
|
@ -391,92 +413,131 @@ frappe.ui.form.on('Data Import', {
|
|||
frm.trigger('show_import_log');
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || '[]');
|
||||
let logs = import_log;
|
||||
frm.toggle_display('import_log', false);
|
||||
frm.toggle_display('import_log_section', logs.length > 0);
|
||||
render_import_log(frm) {
|
||||
frappe.call({
|
||||
'method': 'frappe.client.get_list',
|
||||
'args': {
|
||||
'doctype': 'Data Import Log',
|
||||
'filters': {
|
||||
'data_import': frm.doc.name
|
||||
},
|
||||
'fields': ['success', 'docname', 'messages', 'exception', 'row_indexes'],
|
||||
'limit_page_length': 5000,
|
||||
'order_by': 'log_index'
|
||||
},
|
||||
callback: function(r) {
|
||||
let logs = r.message;
|
||||
|
||||
if (logs.length === 0) {
|
||||
frm.get_field('import_log_preview').$wrapper.empty();
|
||||
if (logs.length === 0) return;
|
||||
|
||||
frm.toggle_display('import_log_section', true);
|
||||
|
||||
let rows = logs
|
||||
.map(log => {
|
||||
let html = '';
|
||||
if (log.success) {
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
html = __('Successfully imported {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
} else {
|
||||
html = __('Successfully updated {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
let messages = (JSON.parse(log.messages || '[]'))
|
||||
.map(JSON.parse)
|
||||
.map(m => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : '';
|
||||
let message = m.message ? `<div>${m.message}</div>` : '';
|
||||
return title + message;
|
||||
})
|
||||
.join('');
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__('Show Traceback')}
|
||||
</button>
|
||||
<div class="collapse" id="${id}" style="margin-top: 15px;">
|
||||
<div class="well">
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
let indicator_color = log.success ? 'green' : 'red';
|
||||
let title = log.success ? __('Success') : __('Failure');
|
||||
|
||||
if (frm.doc.show_failed_logs && log.success) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td>${JSON.parse(log.row_indexes).join(', ')}</td>
|
||||
<td>
|
||||
<div class="indicator ${indicator_color}">${title}</div>
|
||||
</td>
|
||||
<td>
|
||||
${html}
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
${__('No failed logs')}
|
||||
</td></tr>`;
|
||||
}
|
||||
|
||||
frm.get_field('import_log_preview').$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<tr class="text-muted">
|
||||
<th width="10%">${__('Row Number')}</th>
|
||||
<th width="10%">${__('Status')}</th>
|
||||
<th width="80%">${__('Message')}</th>
|
||||
</tr>
|
||||
${rows}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
frm.toggle_display('import_log_section', false);
|
||||
|
||||
if (frm.import_in_progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rows = logs
|
||||
.map(log => {
|
||||
let html = '';
|
||||
if (log.success) {
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
html = __('Successfully imported {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
} else {
|
||||
html = __('Successfully updated {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
}
|
||||
frappe.call({
|
||||
'method': 'frappe.client.get_count',
|
||||
'args': {
|
||||
'doctype': 'Data Import Log',
|
||||
'filters': {
|
||||
'data_import': frm.doc.name
|
||||
}
|
||||
},
|
||||
'callback': function(r) {
|
||||
let count = r.message;
|
||||
if (count < 5000) {
|
||||
frm.trigger('render_import_log');
|
||||
} else {
|
||||
let messages = log.messages
|
||||
.map(JSON.parse)
|
||||
.map(m => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : '';
|
||||
let message = m.message ? `<div>${m.message}</div>` : '';
|
||||
return title + message;
|
||||
})
|
||||
.join('');
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__('Show Traceback')}
|
||||
</button>
|
||||
<div class="collapse" id="${id}" style="margin-top: 15px;">
|
||||
<div class="well">
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
frm.toggle_display('import_log_section', false);
|
||||
frm.add_custom_button(__('Export Import Log'), () =>
|
||||
frm.trigger('export_import_log')
|
||||
);
|
||||
}
|
||||
let indicator_color = log.success ? 'green' : 'red';
|
||||
let title = log.success ? __('Success') : __('Failure');
|
||||
|
||||
if (frm.doc.show_failed_logs && log.success) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td>${log.row_indexes.join(', ')}</td>
|
||||
<td>
|
||||
<div class="indicator ${indicator_color}">${title}</div>
|
||||
</td>
|
||||
<td>
|
||||
${html}
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
${__('No failed logs')}
|
||||
</td></tr>`;
|
||||
}
|
||||
|
||||
frm.get_field('import_log_preview').$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<tr class="text-muted">
|
||||
<th width="10%">${__('Row Number')}</th>
|
||||
<th width="10%">${__('Status')}</th>
|
||||
<th width="80%">${__('Message')}</th>
|
||||
</tr>
|
||||
${rows}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,194 +1,197 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "format:{reference_doctype} Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"import_type",
|
||||
"download_template",
|
||||
"import_file",
|
||||
"html_5",
|
||||
"google_sheets_url",
|
||||
"refresh_google_sheet",
|
||||
"column_break_5",
|
||||
"status",
|
||||
"submit_after_import",
|
||||
"mute_emails",
|
||||
"template_options",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
"section_import_preview",
|
||||
"import_preview",
|
||||
"import_log_section",
|
||||
"import_log",
|
||||
"show_failed_logs",
|
||||
"import_log_preview"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Import Type",
|
||||
"options": "\nInsert New Records\nUpdate Existing Records",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "import_file",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"label": "Import File",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_import_preview",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "template_options",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Options",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log",
|
||||
"fieldtype": "Code",
|
||||
"label": "Import Log",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Log Preview"
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nSuccess\nPartial Success\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "template_warnings",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Warnings",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_after_import",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit After Import",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import File Errors and Warnings"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Warnings"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_template",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Template"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "mute_emails",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Send Emails",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_failed_logs",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Failed Logs"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file",
|
||||
"fieldname": "html_5",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<h5 class=\"text-muted uppercase\">Or</h5>"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
|
||||
"description": "Must be a publicly accessible Google Sheets URL",
|
||||
"fieldname": "google_sheets_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Import from Google Sheets",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
|
||||
"fieldname": "refresh_google_sheet",
|
||||
"fieldtype": "Button",
|
||||
"label": "Refresh Google Sheet"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-11 01:50:42.074623",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
"actions": [],
|
||||
"autoname": "format:{reference_doctype} Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"import_type",
|
||||
"download_template",
|
||||
"import_file",
|
||||
"payload_count",
|
||||
"html_5",
|
||||
"google_sheets_url",
|
||||
"refresh_google_sheet",
|
||||
"column_break_5",
|
||||
"status",
|
||||
"submit_after_import",
|
||||
"mute_emails",
|
||||
"template_options",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
"section_import_preview",
|
||||
"import_preview",
|
||||
"import_log_section",
|
||||
"show_failed_logs",
|
||||
"import_log_preview"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Import Type",
|
||||
"options": "\nInsert New Records\nUpdate Existing Records",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "import_file",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"label": "Import File",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_import_preview",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "template_options",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Options",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Log Preview"
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nSuccess\nPartial Success\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "template_warnings",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Warnings",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_after_import",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit After Import",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import File Errors and Warnings"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Warnings"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_template",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Template"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "mute_emails",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Send Emails",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_failed_logs",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Failed Logs"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file",
|
||||
"fieldname": "html_5",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<h5 class=\"text-muted uppercase\">Or</h5>"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
|
||||
"description": "Must be a publicly accessible Google Sheets URL",
|
||||
"fieldname": "google_sheets_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Import from Google Sheets",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
|
||||
"fieldname": "refresh_google_sheet",
|
||||
"fieldtype": "Button",
|
||||
"label": "Refresh Google Sheet"
|
||||
},
|
||||
{
|
||||
"fieldname": "payload_count",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Payload Count",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-01 20:08:37.624914",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ class DataImport(Document):
|
|||
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
self.set_payload_count()
|
||||
|
||||
def validate_import_file(self):
|
||||
if self.import_file:
|
||||
|
|
@ -38,6 +39,12 @@ class DataImport(Document):
|
|||
return
|
||||
validate_google_sheets_url(self.google_sheets_url)
|
||||
|
||||
def set_payload_count(self):
|
||||
if self.import_file:
|
||||
i = self.get_importer()
|
||||
payloads = i.import_file.get_payloads_for_import()
|
||||
self.payload_count = len(payloads)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(self, import_file=None, google_sheets_url=None):
|
||||
if import_file:
|
||||
|
|
@ -67,7 +74,7 @@ class DataImport(Document):
|
|||
enqueue(
|
||||
start_import,
|
||||
queue="default",
|
||||
timeout=6000,
|
||||
timeout=10000,
|
||||
event="data_import",
|
||||
job_name=self.name,
|
||||
data_import=self.name,
|
||||
|
|
@ -80,6 +87,9 @@ class DataImport(Document):
|
|||
def export_errored_rows(self):
|
||||
return self.get_importer().export_errored_rows()
|
||||
|
||||
def download_import_log(self):
|
||||
return self.get_importer().export_import_log()
|
||||
|
||||
def get_importer(self):
|
||||
return Importer(self.reference_doctype, data_import=self)
|
||||
|
||||
|
|
@ -90,7 +100,6 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
|
|||
import_file, google_sheets_url
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
return frappe.get_doc("Data Import", data_import).start_import()
|
||||
|
|
@ -145,6 +154,30 @@ def download_errored_template(data_import_name):
|
|||
data_import = frappe.get_doc("Data Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_import_log(data_import_name):
|
||||
data_import = frappe.get_doc("Data Import", data_import_name)
|
||||
data_import.download_import_log()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_status(data_import_name):
|
||||
import_status = {}
|
||||
|
||||
logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'],
|
||||
filters={'data_import': data_import_name},
|
||||
group_by='success')
|
||||
|
||||
total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count')
|
||||
|
||||
for log in logs:
|
||||
if log.get('success'):
|
||||
import_status['success'] = log.get('count')
|
||||
else:
|
||||
import_status['failed'] = log.get('count')
|
||||
|
||||
import_status['total_records'] = total_payload_count
|
||||
|
||||
return import_status
|
||||
|
||||
def import_file(
|
||||
doctype, file_path, import_type, submit_after_import=False, console=False
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ frappe.listview_settings['Data Import'] = {
|
|||
'Error': 'red'
|
||||
};
|
||||
let status = doc.status;
|
||||
|
||||
if (imports_in_progress.includes(doc.name)) {
|
||||
status = 'In Progress';
|
||||
}
|
||||
if (status == 'Pending') {
|
||||
status = 'Not Started';
|
||||
}
|
||||
|
||||
return [__(status), colors[status], 'status,=,' + doc.status];
|
||||
},
|
||||
formatters: {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,13 @@ class Importer:
|
|||
)
|
||||
|
||||
def get_data_for_import_preview(self):
|
||||
return self.import_file.get_data_for_import_preview()
|
||||
out = self.import_file.get_data_for_import_preview()
|
||||
|
||||
out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index", limit=10)
|
||||
|
||||
return out
|
||||
|
||||
def before_import(self):
|
||||
# set user lang for translations
|
||||
|
|
@ -58,7 +64,6 @@ class Importer:
|
|||
frappe.flags.in_import = True
|
||||
frappe.flags.mute_emails = self.data_import.mute_emails
|
||||
|
||||
self.data_import.db_set("status", "Pending")
|
||||
self.data_import.db_set("template_warnings", "")
|
||||
|
||||
def import_data(self):
|
||||
|
|
@ -79,20 +84,25 @@ class Importer:
|
|||
return
|
||||
|
||||
# setup import log
|
||||
if self.data_import.import_log:
|
||||
import_log = frappe.parse_json(self.data_import.import_log)
|
||||
else:
|
||||
import_log = []
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
|
||||
# remove previous failures from import log
|
||||
import_log = [log for log in import_log if log.get("success")]
|
||||
log_index = 0
|
||||
|
||||
# Do not remove rows in case of retry after an error or pending data import
|
||||
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count:
|
||||
# remove previous failures from import log only in case of retry after partial success
|
||||
import_log = [log for log in import_log if log.get("success")]
|
||||
|
||||
# get successfully imported rows
|
||||
imported_rows = []
|
||||
for log in import_log:
|
||||
log = frappe._dict(log)
|
||||
if log.success:
|
||||
imported_rows += log.row_indexes
|
||||
if log.success or len(import_log) < self.data_import.payload_count:
|
||||
imported_rows += json.loads(log.row_indexes)
|
||||
|
||||
log_index = log.log_index
|
||||
|
||||
# start import
|
||||
total_payload_count = len(payloads)
|
||||
|
|
@ -146,25 +156,41 @@ class Importer:
|
|||
},
|
||||
)
|
||||
|
||||
import_log.append(
|
||||
frappe._dict(success=True, docname=doc.name, row_indexes=row_indexes)
|
||||
)
|
||||
create_import_log(self.data_import.name, log_index, {
|
||||
'success': True,
|
||||
'docname': doc.name,
|
||||
'row_indexes': row_indexes
|
||||
})
|
||||
|
||||
log_index += 1
|
||||
|
||||
if not self.data_import.status == "Partial Success":
|
||||
self.data_import.db_set("status", "Partial Success")
|
||||
|
||||
# commit after every successful import
|
||||
frappe.db.commit()
|
||||
|
||||
except Exception:
|
||||
import_log.append(
|
||||
frappe._dict(
|
||||
success=False,
|
||||
exception=frappe.get_traceback(),
|
||||
messages=frappe.local.message_log,
|
||||
row_indexes=row_indexes,
|
||||
)
|
||||
)
|
||||
messages = frappe.local.message_log
|
||||
frappe.clear_messages()
|
||||
|
||||
# rollback if exception
|
||||
frappe.db.rollback()
|
||||
|
||||
create_import_log(self.data_import.name, log_index, {
|
||||
'success': False,
|
||||
'exception': frappe.get_traceback(),
|
||||
'messages': messages,
|
||||
'row_indexes': row_indexes
|
||||
})
|
||||
|
||||
log_index += 1
|
||||
|
||||
# Logs are db inserted directly so will have to be fetched again
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
|
||||
# set status
|
||||
failures = [log for log in import_log if not log.get("success")]
|
||||
if len(failures) == total_payload_count:
|
||||
|
|
@ -178,7 +204,6 @@ class Importer:
|
|||
self.print_import_log(import_log)
|
||||
else:
|
||||
self.data_import.db_set("status", status)
|
||||
self.data_import.db_set("import_log", json.dumps(import_log))
|
||||
|
||||
self.after_import()
|
||||
|
||||
|
|
@ -248,11 +273,14 @@ class Importer:
|
|||
if not self.data_import:
|
||||
return
|
||||
|
||||
import_log = frappe.parse_json(self.data_import.import_log or "[]")
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
|
||||
failures = [log for log in import_log if not log.get("success")]
|
||||
row_indexes = []
|
||||
for f in failures:
|
||||
row_indexes.extend(f.get("row_indexes", []))
|
||||
row_indexes.extend(json.loads(f.get("row_indexes", [])))
|
||||
|
||||
# de duplicate
|
||||
row_indexes = list(set(row_indexes))
|
||||
|
|
@ -264,6 +292,30 @@ class Importer:
|
|||
|
||||
build_csv_response(rows, _(self.doctype))
|
||||
|
||||
def export_import_log(self):
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
|
||||
if not self.data_import:
|
||||
return
|
||||
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index")
|
||||
|
||||
header_row = ["Row Numbers", "Status", "Message", "Exception"]
|
||||
|
||||
rows = [header_row]
|
||||
|
||||
for log in import_log:
|
||||
row_number = json.loads(log.get("row_indexes"))[0]
|
||||
status = "Success" if log.get('success') else "Failure"
|
||||
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \
|
||||
log.get("messages")
|
||||
exception = frappe.utils.cstr(log.get("exception", ''))
|
||||
rows += [[row_number, status, message, exception]]
|
||||
|
||||
build_csv_response(rows, self.doctype)
|
||||
|
||||
def print_import_log(self, import_log):
|
||||
failed_records = [log for log in import_log if not log.success]
|
||||
successful_records = [log for log in import_log if log.success]
|
||||
|
|
@ -566,7 +618,7 @@ class Row:
|
|||
)
|
||||
|
||||
# remove standard fields and __islocal
|
||||
for key in frappe.model.default_fields + ("__islocal",):
|
||||
for key in frappe.model.default_fields + frappe.model.child_table_fields + ("__islocal",):
|
||||
doc.pop(key, None)
|
||||
|
||||
for col, value in zip(columns, values):
|
||||
|
|
@ -1172,3 +1224,17 @@ def df_as_json(df):
|
|||
|
||||
def get_select_options(df):
|
||||
return [d for d in (df.options or "").split("\n") if d]
|
||||
|
||||
def create_import_log(data_import, log_index, log_details):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Data Import Log',
|
||||
'log_index': log_index,
|
||||
'success': log_details.get('success'),
|
||||
'data_import': data_import,
|
||||
'row_indexes': json.dumps(log_details.get('row_indexes')),
|
||||
'docname': log_details.get('docname'),
|
||||
'messages': json.dumps(log_details.get('messages', '[]')),
|
||||
'exception': log_details.get('exception')
|
||||
}).db_insert()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import unittest
|
||||
import frappe
|
||||
from frappe.core.doctype.data_import.importer import Importer
|
||||
from frappe.tests.test_query_builder import db_type_is, run_only_if
|
||||
from frappe.utils import getdate, format_duration
|
||||
|
||||
doctype_name = 'DocType for Import'
|
||||
|
|
@ -54,21 +55,27 @@ class TestImporter(unittest.TestCase):
|
|||
self.assertEqual(len(preview.data), 4)
|
||||
self.assertEqual(len(preview.columns), 16)
|
||||
|
||||
# ignored on postgres because myisam doesn't exist on pg
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
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()
|
||||
import_log = frappe.parse_json(data_import.import_log)
|
||||
self.assertEqual(import_log[0]['row_indexes'], [2,3])
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(import_log[1]['row_indexes'], [4])
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['messages'][0])['message'], "Title is required")
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
filters={"data_import": data_import.name},
|
||||
order_by="log_index")
|
||||
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3])
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error)
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['row_indexes']), [4])
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[1]['messages'])[0])['message'], "Title is required")
|
||||
|
||||
def test_data_import_update(self):
|
||||
existing_doc = frappe.get_doc(
|
||||
|
|
|
|||
8
frappe/core/doctype/data_import_log/data_import_log.js
Normal file
8
frappe/core/doctype/data_import_log/data_import_log.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2021, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Data Import Log', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
84
frappe/core/doctype/data_import_log/data_import_log.json
Normal file
84
frappe/core/doctype/data_import_log/data_import_log.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-12-25 16:12:20.205889",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "MyISAM",
|
||||
"field_order": [
|
||||
"data_import",
|
||||
"row_indexes",
|
||||
"success",
|
||||
"docname",
|
||||
"messages",
|
||||
"exception",
|
||||
"log_index"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "data_import",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Data Import",
|
||||
"options": "Data Import"
|
||||
},
|
||||
{
|
||||
"fieldname": "docname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "exception",
|
||||
"fieldtype": "Text",
|
||||
"label": "Exception"
|
||||
},
|
||||
{
|
||||
"fieldname": "row_indexes",
|
||||
"fieldtype": "Code",
|
||||
"label": "Row Indexes",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "success",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Success"
|
||||
},
|
||||
{
|
||||
"fieldname": "log_index",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Log Index"
|
||||
},
|
||||
{
|
||||
"fieldname": "messages",
|
||||
"fieldtype": "Code",
|
||||
"label": "Messages",
|
||||
"options": "JSON"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-29 11:19:19.646076",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
8
frappe/core/doctype/data_import_log/data_import_log.py
Normal file
8
frappe/core/doctype/data_import_log/data_import_log.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DataImportLog(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestDataImportLog(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"hide_days",
|
||||
"hide_seconds",
|
||||
"reqd",
|
||||
"is_virtual",
|
||||
"search_index",
|
||||
"column_break_18",
|
||||
"options",
|
||||
|
|
@ -98,7 +99,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -534,13 +535,19 @@
|
|||
"fieldname": "show_dashboard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Dashboard"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_virtual",
|
||||
"fieldtype": "Check",
|
||||
"label": "Virtual"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-03 11:56:19.812863",
|
||||
"modified": "2022-02-14 11:56:19.812863",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
class Test{classname}(unittest.TestCase):
|
||||
|
||||
class Test{classname}(FrappeTestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -33,9 +33,16 @@ frappe.ui.form.on('DocType', {
|
|||
}
|
||||
}
|
||||
|
||||
const customize_form_link = "<a href='/app/customize-form'>Customize Form</a>";
|
||||
if(!frappe.boot.developer_mode && !frm.doc.custom) {
|
||||
// make the document read-only
|
||||
frm.set_read_only();
|
||||
frm.dashboard.add_comment(__("DocTypes can not be modified, please use {0} instead", [customize_form_link]), "blue", true);
|
||||
} else if (frappe.boot.developer_mode) {
|
||||
let msg = __("This site is running in developer mode. Any change made here will be updated in code.");
|
||||
msg += "<br>";
|
||||
msg += __("If you just want to customize for your site, use {0} instead.", [customize_form_link]);
|
||||
frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
|
||||
if(frm.is_new()) {
|
||||
|
|
@ -54,6 +61,13 @@ frappe.ui.form.on('DocType', {
|
|||
frm.events.set_naming_rule_description(frm);
|
||||
},
|
||||
|
||||
istable: (frm) => {
|
||||
if (frm.doc.istable && frm.is_new()) {
|
||||
frm.set_value('autoname', 'autoincrement');
|
||||
frm.set_value('allow_rename', 0);
|
||||
}
|
||||
},
|
||||
|
||||
naming_rule: function(frm) {
|
||||
// set the "autoname" property based on naming_rule
|
||||
if (frm.doc.naming_rule && !frm.__from_autoname) {
|
||||
|
|
@ -63,6 +77,10 @@ frappe.ui.form.on('DocType', {
|
|||
|
||||
if (frm.doc.naming_rule=='Set by user') {
|
||||
frm.set_value('autoname', 'Prompt');
|
||||
} else if (frm.doc.naming_rule === 'Autoincrement') {
|
||||
frm.set_value('autoname', 'autoincrement');
|
||||
// set allow rename to be false when using autoincrement
|
||||
frm.set_value('allow_rename', 0);
|
||||
} else if (frm.doc.naming_rule=='By fieldname') {
|
||||
frm.set_value('autoname', 'field:');
|
||||
} else if (frm.doc.naming_rule=='By "Naming Series" field') {
|
||||
|
|
@ -84,6 +102,7 @@ frappe.ui.form.on('DocType', {
|
|||
set_naming_rule_description(frm) {
|
||||
let naming_rule_description = {
|
||||
'Set by user': '',
|
||||
'Autoincrement': 'Uses Auto Increment feature of database.<br><b>WARNING: After using this option, any other naming option will not be accessible.</b>',
|
||||
'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist',
|
||||
'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Fieldname called <code>naming_series</code> must exist',
|
||||
'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
|
||||
|
|
@ -104,6 +123,8 @@ frappe.ui.form.on('DocType', {
|
|||
frm.__from_autoname = true;
|
||||
if (frm.doc.autoname.toLowerCase() === 'prompt') {
|
||||
frm.set_value('naming_rule', 'Set by user');
|
||||
} else if (frm.doc.autoname.toLowerCase() === 'autoincrement') {
|
||||
frm.set_value('naming_rule', 'Autoincrement');
|
||||
} else if (frm.doc.autoname.startsWith('field:')) {
|
||||
frm.set_value('naming_rule', 'By fieldname');
|
||||
} else if (frm.doc.autoname.startsWith('naming_series:')) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"allow_auto_repeat",
|
||||
"view_settings",
|
||||
"title_field",
|
||||
"show_title_field_in_link",
|
||||
"search_fields",
|
||||
"default_print_format",
|
||||
"sort_field",
|
||||
|
|
@ -207,7 +208,7 @@
|
|||
"label": "Naming"
|
||||
},
|
||||
{
|
||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
|
||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
|
||||
"fieldname": "autoname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Auto Name",
|
||||
|
|
@ -215,6 +216,7 @@
|
|||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
|
||||
"fieldname": "name_case",
|
||||
"fieldtype": "Select",
|
||||
"label": "Name Case",
|
||||
|
|
@ -281,6 +283,7 @@
|
|||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
|
||||
"fieldname": "allow_rename",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Rename",
|
||||
|
|
@ -564,7 +567,7 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "Naming Rule",
|
||||
"length": 40,
|
||||
"options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
|
||||
"options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
|
||||
},
|
||||
{
|
||||
"fieldname": "migration_hash",
|
||||
|
|
@ -582,10 +585,17 @@
|
|||
"fieldname": "document_states_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Document States"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_title_field_in_link",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Title in Link Fields"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
"idx": 6,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"group": "Views",
|
||||
|
|
@ -663,10 +673,11 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2021-12-09 14:53:10.717788",
|
||||
"modified": "2022-02-15 21:47:16.467217",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -696,5 +707,6 @@
|
|||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -10,7 +10,9 @@ from frappe.cache_manager import clear_user_cache, clear_controller_cache
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import now, cint
|
||||
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
|
||||
from frappe.model import (
|
||||
no_value_fields, default_fields, table_fields, data_field_options, child_table_fields
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
|
@ -58,6 +60,7 @@ class DocType(Document):
|
|||
|
||||
self.check_developer_mode()
|
||||
|
||||
self.validate_autoname()
|
||||
self.validate_name()
|
||||
|
||||
self.set_defaults_for_single_and_table()
|
||||
|
|
@ -74,6 +77,7 @@ class DocType(Document):
|
|||
self.make_amendable()
|
||||
self.make_repeatable()
|
||||
self.validate_nestedset()
|
||||
self.validate_child_table()
|
||||
self.validate_website()
|
||||
self.ensure_minimum_max_attachment_limit()
|
||||
validate_links_table_fieldnames(self)
|
||||
|
|
@ -689,16 +693,51 @@ class DocType(Document):
|
|||
})
|
||||
self.nsm_parent_field = parent_field_name
|
||||
|
||||
def validate_child_table(self):
|
||||
if not self.get("istable") or self.is_new():
|
||||
# if the doctype is not a child table then return
|
||||
# if the doctype is a new doctype and also a child table then
|
||||
# don't move forward as it will be handled via schema
|
||||
return
|
||||
|
||||
self.add_child_table_fields()
|
||||
|
||||
def add_child_table_fields(self):
|
||||
from frappe.database.schema import add_column
|
||||
|
||||
add_column(self.name, "parent", "Data")
|
||||
add_column(self.name, "parenttype", "Data")
|
||||
add_column(self.name, "parentfield", "Data")
|
||||
|
||||
def get_max_idx(self):
|
||||
"""Returns the highest `idx`"""
|
||||
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
|
||||
self.name)
|
||||
return max_idx and max_idx[0][0] or 0
|
||||
|
||||
def validate_autoname(self):
|
||||
if not self.is_new():
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if doc_before_save:
|
||||
if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") \
|
||||
or (self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement"):
|
||||
frappe.throw(_("Cannot change to/from Autoincrement naming rule"))
|
||||
|
||||
else:
|
||||
if self.autoname == "autoincrement":
|
||||
self.allow_rename = 0
|
||||
|
||||
def validate_name(self, name=None):
|
||||
if not name:
|
||||
name = self.name
|
||||
|
||||
# a Doctype name is the tablename created in database
|
||||
# `tab<Doctype Name>` the length of tablename is limited to 64 characters
|
||||
max_length = frappe.db.MAX_COLUMN_LENGTH - 3
|
||||
if len(name) > max_length:
|
||||
# length(tab + <Doctype Name>) should be equal to 64 characters hence doctype should be 61 characters
|
||||
frappe.throw(_("Doctype name is limited to {0} characters ({1})").format(max_length, name), frappe.NameError)
|
||||
|
||||
flags = {"flags": re.ASCII}
|
||||
|
||||
# a DocType name should not start or end with an empty space
|
||||
|
|
@ -706,9 +745,12 @@ class DocType(Document):
|
|||
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
|
||||
# and should only contain letters, numbers and underscore
|
||||
if not re.match(r"^(?![\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)
|
||||
# and should only contain letters, numbers, underscore, and hyphen
|
||||
if not re.match(r"^(?![\W])[^\d_\s][\w -]+$", name, **flags):
|
||||
frappe.throw(_(
|
||||
"A DocType's name should start with a letter and can only "
|
||||
"consist of letters, numbers, spaces, underscores and hyphens"
|
||||
), frappe.NameError, title="Invalid Name")
|
||||
|
||||
validate_route_conflict(self.doctype, self.name)
|
||||
|
||||
|
|
@ -755,29 +797,39 @@ def validate_series(dt, autoname=None, name=None):
|
|||
|
||||
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
|
||||
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures:
|
||||
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 = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
|
||||
fieldnames = tuple(field.fieldname for field in meta.fields)
|
||||
for index, link in enumerate(meta.links, 1):
|
||||
if not frappe.get_meta(link.link_doctype).has_field(link.link_fieldname):
|
||||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(
|
||||
index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)
|
||||
)
|
||||
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
|
||||
|
||||
if link.is_child_table and not meta.get_field(link.table_fieldname):
|
||||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
|
||||
if not link.is_child_table:
|
||||
continue
|
||||
|
||||
if not link.parent_doctype:
|
||||
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index)
|
||||
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
|
||||
|
||||
if not link.table_fieldname:
|
||||
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index)
|
||||
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
|
||||
|
||||
if meta.name == link.parent_doctype:
|
||||
field_exists = link.table_fieldname in fieldnames
|
||||
else:
|
||||
field_exists = frappe.get_meta(link.parent_doctype).has_field(link.table_fieldname)
|
||||
|
||||
if not field_exists:
|
||||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(
|
||||
index, frappe.bold(link.table_fieldname), frappe.bold(meta.name)
|
||||
)
|
||||
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
|
||||
|
||||
if link.is_child_table:
|
||||
if not link.parent_doctype:
|
||||
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
|
||||
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
|
||||
|
||||
if not link.table_fieldname:
|
||||
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
|
||||
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
|
||||
|
||||
def validate_fields_for_doctype(doctype):
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
validate_links_table_fieldnames(meta)
|
||||
|
|
@ -1009,7 +1061,7 @@ def validate_fields(meta):
|
|||
sort_fields = [d.split()[0] for d in meta.sort_field.split(',')]
|
||||
|
||||
for fieldname in sort_fields:
|
||||
if not fieldname in fieldname_list + list(default_fields):
|
||||
if fieldname not in (fieldname_list + list(default_fields) + list(child_table_fields)):
|
||||
frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname),
|
||||
InvalidFieldNameError)
|
||||
|
||||
|
|
@ -1050,6 +1102,9 @@ def validate_fields(meta):
|
|||
field.fetch_from = field.fetch_from.strip('\n').strip()
|
||||
|
||||
def validate_data_field_type(docfield):
|
||||
if docfield.get("is_virtual"):
|
||||
return
|
||||
|
||||
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
|
||||
if docfield.options and (docfield.options not in data_field_options):
|
||||
df_str = frappe.bold(_(docfield.label))
|
||||
|
|
@ -1295,10 +1350,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
|
|||
else:
|
||||
raise
|
||||
|
||||
def check_fieldname_conflicts(doctype, fieldname):
|
||||
def check_fieldname_conflicts(docfield):
|
||||
"""Checks if fieldname conflicts with methods or properties"""
|
||||
|
||||
doc = frappe.get_doc({"doctype": doctype})
|
||||
doc = frappe.get_doc({"doctype": docfield.dt})
|
||||
available_objects = [x for x in dir(doc) if isinstance(x, str)]
|
||||
property_list = [
|
||||
x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
|
||||
|
|
@ -1306,9 +1360,10 @@ def check_fieldname_conflicts(doctype, fieldname):
|
|||
method_list = [
|
||||
x for x in available_objects if x not in property_list and callable(getattr(doc, x))
|
||||
]
|
||||
msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname)
|
||||
|
||||
if fieldname in method_list + property_list:
|
||||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
|
||||
if docfield.fieldname in method_list + property_list:
|
||||
frappe.msgprint(msg, raise_exception=not docfield.is_virtual)
|
||||
|
||||
def clear_linked_doctype_cache():
|
||||
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ class TestDocType(unittest.TestCase):
|
|||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
|
||||
for name in ("Some DocType", "Some_DocType"):
|
||||
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert)
|
||||
for name in ("Some DocType", "Some_DocType", "Some-DocType"):
|
||||
if frappe.db.exists("DocType", name):
|
||||
frappe.delete_doc("DocType", name)
|
||||
|
||||
|
|
@ -353,7 +354,6 @@ class TestDocType(unittest.TestCase):
|
|||
dump_docs = json.dumps(docs.get('docs'))
|
||||
cancel_all_linked_docs(dump_docs)
|
||||
data_link_doc.cancel()
|
||||
data_doc.name = '{}-CANC-0'.format(data_doc.name)
|
||||
data_doc.load_from_db()
|
||||
self.assertEqual(data_link_doc.docstatus, 2)
|
||||
self.assertEqual(data_doc.docstatus, 2)
|
||||
|
|
@ -377,7 +377,7 @@ class TestDocType(unittest.TestCase):
|
|||
for data in link_doc.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
link_doc.insert(ignore_if_duplicate=True)
|
||||
link_doc.insert()
|
||||
|
||||
#create first parent doctype
|
||||
test_doc_1 = new_doctype('Test Doctype 1')
|
||||
|
|
@ -392,7 +392,7 @@ class TestDocType(unittest.TestCase):
|
|||
for data in test_doc_1.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
test_doc_1.insert(ignore_if_duplicate=True)
|
||||
test_doc_1.insert()
|
||||
|
||||
#crete second parent doctype
|
||||
doc = new_doctype('Test Doctype 2')
|
||||
|
|
@ -407,7 +407,7 @@ class TestDocType(unittest.TestCase):
|
|||
for data in link_doc.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
doc.insert(ignore_if_duplicate=True)
|
||||
doc.insert()
|
||||
|
||||
# create doctype data
|
||||
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
|
||||
|
|
@ -438,7 +438,6 @@ class TestDocType(unittest.TestCase):
|
|||
# checking that doc for Test Doctype 2 is not canceled
|
||||
self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel)
|
||||
|
||||
data_doc_2.name = '{}-CANC-0'.format(data_doc_2.name)
|
||||
data_doc.load_from_db()
|
||||
data_doc_2.load_from_db()
|
||||
self.assertEqual(data_link_doc_1.docstatus, 2)
|
||||
|
|
@ -499,7 +498,30 @@ class TestDocType(unittest.TestCase):
|
|||
self.assertEqual(doc.is_virtual, 1)
|
||||
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))
|
||||
|
||||
def new_doctype(name, unique=0, depends_on='', fields=None):
|
||||
def test_default_fieldname(self):
|
||||
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}]
|
||||
dt = new_doctype("DT with default field", fields=fields)
|
||||
dt.insert()
|
||||
|
||||
dt.delete()
|
||||
|
||||
def test_autoincremented_doctype_transition(self):
|
||||
frappe.delete_doc("testy_autoinc_dt")
|
||||
dt = new_doctype("testy_autoinc_dt", autoincremented=True).insert(ignore_permissions=True)
|
||||
dt.autoname = "hash"
|
||||
|
||||
try:
|
||||
dt.save(ignore_permissions=True)
|
||||
except frappe.ValidationError as e:
|
||||
self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule")
|
||||
else:
|
||||
self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule")
|
||||
finally:
|
||||
# cleanup
|
||||
dt.delete(ignore_permissions=True)
|
||||
|
||||
|
||||
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
|
|
@ -515,7 +537,8 @@ def new_doctype(name, unique=0, depends_on='', fields=None):
|
|||
"role": "System Manager",
|
||||
"read": 1,
|
||||
}],
|
||||
"name": name
|
||||
"name": name,
|
||||
"autoname": "autoincrement" if autoincremented else ""
|
||||
})
|
||||
|
||||
if fields:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import evaluate_filters
|
||||
from frappe.model.naming import parse_naming_series
|
||||
from frappe import _
|
||||
|
||||
class DocumentNamingRule(Document):
|
||||
|
|
@ -27,7 +28,9 @@ class DocumentNamingRule(Document):
|
|||
return
|
||||
|
||||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
|
||||
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
|
||||
naming_series = parse_naming_series(self.prefix, doc=doc)
|
||||
|
||||
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
|
||||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
"""
|
||||
|
|
@ -7,7 +7,6 @@ record of files
|
|||
naming for same name files: file.gif, file-1.gif, file-2.gif etc
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import imghdr
|
||||
import io
|
||||
|
|
@ -17,9 +16,10 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
from requests.exceptions import HTTPError, SSLError
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from io import BytesIO
|
||||
from urllib.parse import quote, unquote
|
||||
|
|
@ -31,6 +31,11 @@ from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, g
|
|||
from frappe.utils.image import strip_exif_data, optimize_image
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL.ImageFile import ImageFile
|
||||
from requests.models import Response
|
||||
|
||||
|
||||
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
|
@ -276,7 +281,7 @@ class File(Document):
|
|||
image, filename, extn = get_local_image(self.file_url)
|
||||
else:
|
||||
image, filename, extn = get_web_image(self.file_url)
|
||||
except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
|
||||
except (HTTPError, SSLError, IOError, TypeError):
|
||||
return
|
||||
|
||||
size = width, height
|
||||
|
|
@ -572,12 +577,10 @@ class File(Document):
|
|||
|
||||
@staticmethod
|
||||
def zip_files(files):
|
||||
from six import string_types
|
||||
|
||||
zip_file = io.BytesIO()
|
||||
zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED)
|
||||
for _file in files:
|
||||
if isinstance(_file, string_types):
|
||||
if isinstance(_file, str):
|
||||
_file = frappe.get_doc("File", _file)
|
||||
if not isinstance(_file, File):
|
||||
continue
|
||||
|
|
@ -650,9 +653,17 @@ def setup_folder_path(filename, new_parent):
|
|||
from frappe.model.rename_doc import rename_doc
|
||||
rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True)
|
||||
|
||||
def get_extension(filename, extn, content):
|
||||
def get_extension(filename, extn, content: bytes = None, response: "Response" = None) -> str:
|
||||
mimetype = None
|
||||
|
||||
if response:
|
||||
content_type = response.headers.get("Content-Type")
|
||||
|
||||
if content_type:
|
||||
_extn = mimetypes.guess_extension(content_type)
|
||||
if _extn:
|
||||
return _extn[1:]
|
||||
|
||||
if extn:
|
||||
# remove '?' char and parameters from extn if present
|
||||
if '?' in extn:
|
||||
|
|
@ -695,14 +706,14 @@ def get_local_image(file_url):
|
|||
|
||||
return image, filename, extn
|
||||
|
||||
def get_web_image(file_url):
|
||||
def get_web_image(file_url: str) -> Tuple["ImageFile", str, str]:
|
||||
# download
|
||||
file_url = frappe.utils.get_url(file_url)
|
||||
r = requests.get(file_url, stream=True)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if "404" in e.args[0]:
|
||||
except HTTPError:
|
||||
if r.status_code == 404:
|
||||
frappe.msgprint(_("File '{0}' not found").format(file_url))
|
||||
else:
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
|
||||
|
|
@ -721,7 +732,10 @@ def get_web_image(file_url):
|
|||
filename = get_random_filename()
|
||||
extn = None
|
||||
|
||||
extn = get_extension(filename, extn, r.content)
|
||||
extn = get_extension(filename, extn, response=r)
|
||||
if extn == "bin":
|
||||
extn = get_extension(filename, extn, content=r.content) or "png"
|
||||
|
||||
filename = "/files/" + strip(unquote(filename))
|
||||
|
||||
return image, filename, extn
|
||||
|
|
@ -731,7 +745,7 @@ def delete_file(path):
|
|||
"""Delete file from `public folder`"""
|
||||
if path:
|
||||
if ".." in path.split("/"):
|
||||
frappe.msgprint(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))
|
||||
frappe.throw(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))
|
||||
|
||||
parts = os.path.split(path.strip("/"))
|
||||
if parts[0]=="files":
|
||||
|
|
@ -864,8 +878,9 @@ def extract_images_from_html(doc, content, is_private=False):
|
|||
else:
|
||||
filename = get_random_filename(content_type=mtype)
|
||||
|
||||
doctype = doc.parenttype if doc.parent else doc.doctype
|
||||
name = doc.parent or doc.name
|
||||
# attaching a file to a child table doc, attaches it to the parent doc
|
||||
doctype = doc.parenttype if doc.get("parent") else doc.doctype
|
||||
name = doc.get("parent") or doc.name
|
||||
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import base64
|
||||
import json
|
||||
import frappe
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from frappe import _
|
||||
from frappe.core.doctype.file.file import get_attached_images, move_file, get_files_in_folder, unzip_file
|
||||
from frappe.core.doctype.file.file import File, get_attached_images, move_file, get_files_in_folder, unzip_file
|
||||
from frappe.utils import get_files_path
|
||||
# test_records = frappe.get_test_records('File')
|
||||
|
||||
test_content1 = 'Hello'
|
||||
test_content2 = 'Hello World'
|
||||
|
|
@ -24,8 +23,6 @@ def make_test_doc():
|
|||
|
||||
|
||||
class TestSimpleFile(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
self.test_content = test_content1
|
||||
|
|
@ -38,21 +35,13 @@ class TestSimpleFile(unittest.TestCase):
|
|||
_file.save()
|
||||
self.saved_file_url = _file.file_url
|
||||
|
||||
|
||||
def test_save(self):
|
||||
_file = frappe.get_doc("File", {"file_url": self.saved_file_url})
|
||||
content = _file.get_content()
|
||||
self.assertEqual(content, self.test_content)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# File gets deleted on rollback, so blank
|
||||
pass
|
||||
|
||||
|
||||
class TestBase64File(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
self.test_content = base64.b64encode(test_content1.encode('utf-8'))
|
||||
|
|
@ -66,18 +55,12 @@ class TestBase64File(unittest.TestCase):
|
|||
_file.save()
|
||||
self.saved_file_url = _file.file_url
|
||||
|
||||
|
||||
def test_saved_content(self):
|
||||
_file = frappe.get_doc("File", {"file_url": self.saved_file_url})
|
||||
content = _file.get_content()
|
||||
self.assertEqual(content, test_content1)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# File gets deleted on rollback, so blank
|
||||
pass
|
||||
|
||||
|
||||
class TestSameFileName(unittest.TestCase):
|
||||
def test_saved_content(self):
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
|
|
@ -130,8 +113,6 @@ class TestSameFileName(unittest.TestCase):
|
|||
|
||||
|
||||
class TestSameContent(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.attached_to_doctype1, self.attached_to_docname1 = make_test_doc()
|
||||
self.attached_to_doctype2, self.attached_to_docname2 = make_test_doc()
|
||||
|
|
@ -186,10 +167,6 @@ class TestSameContent(unittest.TestCase):
|
|||
limit_property.delete()
|
||||
frappe.clear_cache(doctype='ToDo')
|
||||
|
||||
def tearDown(self):
|
||||
# File gets deleted on rollback, so blank
|
||||
pass
|
||||
|
||||
|
||||
class TestFile(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -398,29 +375,39 @@ class TestFile(unittest.TestCase):
|
|||
|
||||
def test_make_thumbnail(self):
|
||||
# test web image
|
||||
test_file = frappe.get_doc({
|
||||
test_file: File = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'logo',
|
||||
"file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
test_file.make_thumbnail()
|
||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
|
||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg')
|
||||
|
||||
# test web image without extension
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'logo',
|
||||
"file_url": frappe.utils.get_url('/_test/assets/image'),
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
test_file.make_thumbnail()
|
||||
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg"))
|
||||
|
||||
# test local image
|
||||
test_file.db_set('thumbnail_url', None)
|
||||
test_file.reload()
|
||||
test_file.file_url = "/files/image_small.jpg"
|
||||
test_file.make_thumbnail(suffix="xs", crop=True)
|
||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
|
||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg')
|
||||
|
||||
frappe.clear_messages()
|
||||
test_file.db_set('thumbnail_url', None)
|
||||
test_file.reload()
|
||||
test_file.file_url = frappe.utils.get_url('unknown.jpg')
|
||||
test_file.make_thumbnail(suffix="xs")
|
||||
self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"})
|
||||
self.assertEquals(test_file.thumbnail_url, None)
|
||||
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
|
||||
self.assertEqual(test_file.thumbnail_url, None)
|
||||
|
||||
def test_file_unzip(self):
|
||||
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
|
||||
|
|
|
|||
|
|
@ -1,154 +1,55 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:gateway",
|
||||
"beta": 0,
|
||||
"creation": "2015-12-15 22:26:45.221162",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"autoname": "field:gateway",
|
||||
"creation": "2022-01-24 21:09:47.229371",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"gateway",
|
||||
"gateway_settings",
|
||||
"gateway_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Gateway",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "gateway",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Gateway",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway_settings",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Gateway Settings",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "gateway_settings",
|
||||
"fieldtype": "Link",
|
||||
"label": "Gateway Settings",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway_controller",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Gateway Controller",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "gateway_settings",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "gateway_controller",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Gateway Controller",
|
||||
"options": "gateway_settings"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 1,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-02-05 14:24:33.526645",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Payment Gateway",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2022-01-24 21:17:03.864719",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Payment Gateway",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ class Report(Document):
|
|||
delete_permanently=True)
|
||||
|
||||
def get_columns(self):
|
||||
return [d.as_dict(no_default_fields = True) for d in self.columns]
|
||||
return [d.as_dict(no_default_fields=True, no_child_table_fields=True) for d in self.columns]
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_doctype_roles(self):
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
import frappe, json, os
|
||||
import unittest
|
||||
from frappe.desk.query_report import run, save_report
|
||||
from frappe.desk.query_report import run, save_report, add_total_row
|
||||
from frappe.desk.reportview import delete_report, save_report as _save_report
|
||||
from frappe.custom.doctype.customize_form.customize_form import reset_customization
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
|
||||
test_records = frappe.get_test_records('Report')
|
||||
test_dependencies = ['User']
|
||||
|
|
@ -30,6 +32,60 @@ class TestReport(unittest.TestCase):
|
|||
self.assertEqual(columns[1].get('label'), 'Module')
|
||||
self.assertTrue('User' in [d.get('name') for d in data])
|
||||
|
||||
def test_save_or_delete_report(self):
|
||||
'''Test for validations when editing / deleting report of type Report Builder'''
|
||||
|
||||
try:
|
||||
report = frappe.get_doc({
|
||||
'doctype': 'Report',
|
||||
'ref_doctype': 'User',
|
||||
'report_name': 'Test Delete Report',
|
||||
'report_type': 'Report Builder',
|
||||
'is_standard': 'No',
|
||||
}).insert()
|
||||
|
||||
# Check for PermissionError
|
||||
create_user("test_report_owner@example.com", "Website Manager")
|
||||
frappe.set_user("test_report_owner@example.com")
|
||||
self.assertRaises(frappe.PermissionError, delete_report, report.name)
|
||||
|
||||
# Check for Report Type
|
||||
frappe.set_user("Administrator")
|
||||
report.db_set("report_type", "Custom Report")
|
||||
self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"Only reports of type Report Builder can be deleted",
|
||||
delete_report,
|
||||
report.name
|
||||
)
|
||||
|
||||
# Check if creating and deleting works with proper validations
|
||||
frappe.set_user("test@example.com")
|
||||
report_name = _save_report(
|
||||
'Dummy Report',
|
||||
'User',
|
||||
json.dumps([{
|
||||
'fieldname': 'email',
|
||||
'fieldtype': 'Data',
|
||||
'label': 'Email',
|
||||
'insert_after_index': 0,
|
||||
'link_field': 'name',
|
||||
'doctype': 'User',
|
||||
'options': 'Email',
|
||||
'width': 100,
|
||||
'id':'email',
|
||||
'name': 'Email'
|
||||
}])
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Report", report_name)
|
||||
delete_report(doc.name)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def test_custom_report(self):
|
||||
reset_customization('User')
|
||||
custom_report_name = save_report(
|
||||
|
|
@ -226,3 +282,56 @@ result = [
|
|||
|
||||
# Set user back to administrator
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
def test_add_total_row_for_tree_reports(self):
|
||||
report_settings = {
|
||||
'tree': True,
|
||||
'parent_field': 'parent_value'
|
||||
}
|
||||
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "parent_column",
|
||||
"label": "Parent Column",
|
||||
"fieldtype": "Data",
|
||||
"width": 10
|
||||
},
|
||||
{
|
||||
"fieldname": "column_1",
|
||||
"label": "Column 1",
|
||||
"fieldtype": "Float",
|
||||
"width": 10
|
||||
},
|
||||
{
|
||||
"fieldname": "column_2",
|
||||
"label": "Column 2",
|
||||
"fieldtype": "Float",
|
||||
"width": 10
|
||||
}
|
||||
]
|
||||
|
||||
result = [
|
||||
{
|
||||
"parent_column": "Parent 1",
|
||||
"column_1": 200,
|
||||
"column_2": 150.50
|
||||
},
|
||||
{
|
||||
"parent_column": "Child 1",
|
||||
"column_1": 100,
|
||||
"column_2": 75.25,
|
||||
"parent_value": "Parent 1"
|
||||
},
|
||||
{
|
||||
"parent_column": "Child 2",
|
||||
"column_1": 100,
|
||||
"column_2": 75.25,
|
||||
"parent_value": "Parent 1"
|
||||
}
|
||||
]
|
||||
|
||||
result = add_total_row(result, columns, meta=None, is_tree=report_settings['tree'],
|
||||
parent_field=report_settings['parent_field'])
|
||||
self.assertEqual(result[-1][0], "Total")
|
||||
self.assertEqual(result[-1][1], 200)
|
||||
self.assertEqual(result[-1][2], 150.50)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class Role(Document):
|
|||
|
||||
def get_info_based_on_role(role, field='email'):
|
||||
''' Get information of all users that have been assigned this role '''
|
||||
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
|
||||
users = frappe.get_list("Has Role", filters={"role": role}, parent_doctype="User",
|
||||
fields=["parent as user_name"])
|
||||
|
||||
return get_user_info(users, field)
|
||||
|
|
|
|||
|
|
@ -34,19 +34,7 @@ def run_server_script_for_doc_event(doc, event):
|
|||
if scripts:
|
||||
# run all scripts for this doctype + event
|
||||
for script_name in scripts:
|
||||
try:
|
||||
frappe.get_doc('Server Script', script_name).execute_doc(doc)
|
||||
except Exception as e:
|
||||
message = frappe._('Error executing Server Script {0}. Open Browser Console to see traceback.').format(
|
||||
frappe.utils.get_link_to_form('Server Script', script_name)
|
||||
)
|
||||
exception = type(e)
|
||||
if getattr(frappe, 'request', None):
|
||||
# all exceptions throw 500 which is internal server error
|
||||
# however server script error is a user error
|
||||
# so we should throw 417 which is expectation failed
|
||||
exception.http_status_code = 417
|
||||
frappe.throw(title=frappe._('Server Script Error'), msg=message, exc=exception)
|
||||
frappe.get_doc('Server Script', script_name).execute_doc(doc)
|
||||
|
||||
def get_server_script_map():
|
||||
# fetch cached server script methods
|
||||
|
|
|
|||
|
|
@ -112,7 +112,10 @@ class TestServerScript(unittest.TestCase):
|
|||
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
|
||||
|
||||
def test_permission_query(self):
|
||||
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
|
||||
if frappe.conf.db_type == "mariadb":
|
||||
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
|
||||
else:
|
||||
self.assertTrue('where (1 = \'1\')' in frappe.db.get_list('ToDo', run=False))
|
||||
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
|
||||
|
||||
def test_attribute_error(self):
|
||||
|
|
@ -139,3 +142,42 @@ class TestServerScript(unittest.TestCase):
|
|||
|
||||
server_script.disabled = 1
|
||||
server_script.save()
|
||||
|
||||
def test_restricted_qb(self):
|
||||
todo = frappe.get_doc(doctype="ToDo", description="QbScriptTestNote")
|
||||
todo.insert()
|
||||
|
||||
script = frappe.get_doc(
|
||||
doctype='Server Script',
|
||||
name='test_qb_restrictions',
|
||||
script_type = 'API',
|
||||
api_method = 'test_qb_restrictions',
|
||||
allow_guest = 1,
|
||||
# whitelisted update
|
||||
script = f'''
|
||||
frappe.db.set_value("ToDo", "{todo.name}", "description", "safe")
|
||||
'''
|
||||
)
|
||||
script.insert()
|
||||
script.execute_method()
|
||||
|
||||
todo.reload()
|
||||
self.assertEqual(todo.description, "safe")
|
||||
|
||||
# unsafe update
|
||||
script.script = f"""
|
||||
todo = frappe.qb.DocType("ToDo")
|
||||
frappe.qb.update(todo).set(todo.description, "unsafe").where(todo.name == "{todo.name}").run()
|
||||
"""
|
||||
script.save()
|
||||
self.assertRaises(frappe.PermissionError, script.execute_method)
|
||||
todo.reload()
|
||||
self.assertEqual(todo.description, "safe")
|
||||
|
||||
# safe select
|
||||
script.script = f"""
|
||||
todo = frappe.qb.DocType("ToDo")
|
||||
frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
|
||||
"""
|
||||
script.save()
|
||||
script.execute_method()
|
||||
|
|
|
|||
|
|
@ -31,4 +31,15 @@ class test(Document):
|
|||
def get_value(self, fields, filters, **kwargs):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
return [json.load(read_file)]
|
||||
|
||||
def get_count(self, args):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
|
||||
def get_stats(self, args):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
|
||||
|
|
|
|||
|
|
@ -355,7 +355,11 @@ class TestUser(unittest.TestCase):
|
|||
test_user.reload()
|
||||
self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/")
|
||||
update_password(old_password, old_password=new_password)
|
||||
self.assertEqual(json.loads(frappe.message_log[0]), {"message": "Password reset instructions have been sent to your email"})
|
||||
self.assertEqual(
|
||||
json.loads(frappe.message_log[0]).get("message"),
|
||||
"Password reset instructions have been sent to your email"
|
||||
)
|
||||
|
||||
sendmail.assert_called_once()
|
||||
self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")
|
||||
|
||||
|
|
|
|||
|
|
@ -668,8 +668,7 @@
|
|||
"link_fieldname": "user"
|
||||
}
|
||||
],
|
||||
"max_attachments": 5,
|
||||
"modified": "2022-01-03 11:53:25.250822",
|
||||
"modified": "2022-03-09 01:47:56.745069",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
|
|||
|
|
@ -253,8 +253,8 @@ class User(Document):
|
|||
self.email_new_password(new_password)
|
||||
|
||||
except frappe.OutgoingEmailError:
|
||||
print(frappe.get_traceback())
|
||||
pass # email server not set, don't send email
|
||||
# email server not set, don't send email
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
||||
@Document.hook
|
||||
def validate_reset_password(self):
|
||||
|
|
@ -344,7 +344,7 @@ class User(Document):
|
|||
|
||||
frappe.sendmail(recipients=self.email, sender=sender, subject=subject,
|
||||
template=template, args=args, header=[subject, "green"],
|
||||
delayed=(not now) if now!=None else self.flags.delay_emails, retry=3)
|
||||
delayed=(not now) if now is not None else self.flags.delay_emails, retry=3)
|
||||
|
||||
def a_system_manager_should_exist(self):
|
||||
if not self.get_other_system_managers():
|
||||
|
|
@ -756,7 +756,7 @@ def verify_password(password):
|
|||
@frappe.whitelist(allow_guest=True)
|
||||
def sign_up(email, full_name, redirect_to):
|
||||
if is_signup_disabled():
|
||||
frappe.throw(_('Sign Up is disabled'), title='Not Allowed')
|
||||
frappe.throw(_("Sign Up is disabled"), title=_("Not Allowed"))
|
||||
|
||||
user = frappe.db.get("User", {"email": email})
|
||||
if user:
|
||||
|
|
@ -810,8 +810,10 @@ def reset_password(user):
|
|||
user.validate_reset_password()
|
||||
user.reset_password(send_email=True)
|
||||
|
||||
return frappe.msgprint(_("Password reset instructions have been sent to your email"))
|
||||
|
||||
return frappe.msgprint(
|
||||
msg=_("Password reset instructions have been sent to your email"),
|
||||
title=_("Password Email Sent")
|
||||
)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.local.response['http_status_code'] = 400
|
||||
frappe.clear_messages()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from frappe.core.doctype.user_permission.user_permission import add_user_permissions, remove_applicable
|
||||
from frappe.permissions import has_user_permission
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
|
|
@ -31,6 +32,18 @@ class TestUserPermission(unittest.TestCase):
|
|||
param = get_params(user, 'User', perm_user.name, is_default=1)
|
||||
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
|
||||
|
||||
def test_default_user_permission_corectness(self):
|
||||
user = create_user('test_default_corectness_permission_1@example.com')
|
||||
param = get_params(user, 'User', user.name, is_default=1, hide_descendants= 1)
|
||||
add_user_permissions(param)
|
||||
#create a duplicate entry with default
|
||||
perm_user = create_user('test_default_corectness2@example.com')
|
||||
test_blog = make_test_blog()
|
||||
param = get_params(perm_user, 'Blog Post', test_blog.name, is_default=1, hide_descendants= 1)
|
||||
add_user_permissions(param)
|
||||
frappe.db.delete('User Permission', filters={'for_value': test_blog.name})
|
||||
frappe.delete_doc('Blog Post', test_blog.name)
|
||||
|
||||
def test_default_user_permission(self):
|
||||
frappe.set_user('Administrator')
|
||||
user = create_user('test_user_perm1@example.com', 'Website Manager')
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ frappe.ui.form.on('User Permission', {
|
|||
|
||||
set_applicable_for_constraint: frm => {
|
||||
frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes);
|
||||
|
||||
if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) {
|
||||
frm.set_value('applicable_for', null);
|
||||
frm.set_value('applicable_for', null, null, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ class UserPermission(Document):
|
|||
}, or_filters={
|
||||
'applicable_for': cstr(self.applicable_for),
|
||||
'apply_to_all_doctypes': 1,
|
||||
'hide_descendants': cstr(self.hide_descendants)
|
||||
}, limit=1)
|
||||
if overlap_exists:
|
||||
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
class TestUserType(unittest.TestCase):
|
||||
pass
|
||||
def setUp(self):
|
||||
create_role()
|
||||
|
||||
def test_add_select_perm_doctypes(self):
|
||||
user_type = create_user_type('Test User Type')
|
||||
|
||||
# select perms added for all link fields
|
||||
doc = frappe.get_meta('Contact')
|
||||
link_fields = doc.get_link_fields()
|
||||
select_doctypes = frappe.get_all('User Select Document Type', {'parent': user_type.name}, pluck='document_type')
|
||||
|
||||
for entry in link_fields:
|
||||
self.assertTrue(entry.options in select_doctypes)
|
||||
|
||||
# select perms added for all child table link fields
|
||||
link_fields = []
|
||||
for child_table in doc.get_table_fields():
|
||||
child_doc = frappe.get_meta(child_table.options)
|
||||
link_fields.extend(child_doc.get_link_fields())
|
||||
|
||||
for entry in link_fields:
|
||||
self.assertTrue(entry.options in select_doctypes)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_user_type(user_type):
|
||||
if frappe.db.exists('User Type', user_type):
|
||||
frappe.delete_doc('User Type', user_type)
|
||||
|
||||
user_type_limit = {frappe.scrub(user_type): 1}
|
||||
update_site_config('user_type_doctype_limit', user_type_limit)
|
||||
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'User Type',
|
||||
'name': user_type,
|
||||
'role': '_Test User Type',
|
||||
'user_id_field': 'user',
|
||||
'apply_user_permission_on': 'User'
|
||||
})
|
||||
|
||||
doc.append('user_doctypes', {
|
||||
'document_type': 'Contact',
|
||||
'read': 1,
|
||||
'write': 1
|
||||
})
|
||||
|
||||
return doc.insert()
|
||||
|
||||
|
||||
def create_role():
|
||||
if not frappe.db.exists('Role', '_Test User Type'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Role',
|
||||
'role_name': '_Test User Type',
|
||||
'desk_access': 1,
|
||||
'is_custom': 1
|
||||
}).insert()
|
||||
|
|
@ -121,7 +121,7 @@ class UserType(Document):
|
|||
|
||||
for child_table in doc.get_table_fields():
|
||||
child_doc = frappe.get_meta(child_table.options)
|
||||
if not child_doc.istable:
|
||||
if child_doc:
|
||||
self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes)
|
||||
|
||||
if select_doctypes:
|
||||
|
|
@ -193,7 +193,7 @@ def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters
|
|||
['DocType', 'read_only', '=', 0], ['DocType', 'name', 'like', '%{0}%'.format(txt)]]
|
||||
|
||||
doctypes = frappe.get_all('DocType', fields = ['`tabDocType`.`name`'], filters=filters,
|
||||
order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1)
|
||||
order_by='`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1)
|
||||
|
||||
custom_dt_filters = [['Custom Field', 'dt', 'like', '%{0}%'.format(txt)],
|
||||
['Custom Field', 'options', '=', 'User'], ['Custom Field', 'fieldtype', '=', 'Link']]
|
||||
|
|
|
|||
|
|
@ -39,43 +39,3 @@ def get_todays_events(as_list=False):
|
|||
today = nowdate()
|
||||
events = get_events(today, today)
|
||||
return events if as_list else len(events)
|
||||
|
||||
def get_unseen_likes():
|
||||
"""Returns count of unseen likes"""
|
||||
|
||||
comment_doctype = DocType("Comment")
|
||||
return frappe.db.count(comment_doctype,
|
||||
filters=(
|
||||
(comment_doctype.comment_type == "Like")
|
||||
& (comment_doctype.modified >= Now() - Interval(years=1))
|
||||
& (comment_doctype.owner.notnull())
|
||||
& (comment_doctype.owner != frappe.session.user)
|
||||
& (comment_doctype.reference_owner == frappe.session.user)
|
||||
& (comment_doctype.seen == 0)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_unread_emails():
|
||||
"returns count of unread emails for a user"
|
||||
|
||||
communication_doctype = DocType("Communication")
|
||||
user_doctype = DocType("User")
|
||||
distinct_email_accounts = (
|
||||
frappe.qb.from_(user_doctype)
|
||||
.select(user_doctype.email_account)
|
||||
.where(user_doctype.parent == frappe.session.user)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
return frappe.db.count(communication_doctype,
|
||||
filters=(
|
||||
(communication_doctype.communication_type == "Communication")
|
||||
& (communication_doctype.communication_medium == "Email")
|
||||
& (communication_doctype.sent_or_received == "Received")
|
||||
& (communication_doctype.email_status.notin(["spam", "Trash"]))
|
||||
& (communication_doctype.email_account.isin(distinct_email_accounts))
|
||||
& (communication_doctype.modified >= Now() - Interval(years=1))
|
||||
& (communication_doctype.seen == 0)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class Dashboard {
|
|||
|
||||
show() {
|
||||
this.route = frappe.get_route();
|
||||
this.set_breadcrumbs();
|
||||
if (this.route.length > 1) {
|
||||
// from route
|
||||
this.show_dashboard(this.route.slice(-1)[0]);
|
||||
|
|
@ -75,6 +76,10 @@ class Dashboard {
|
|||
frappe.last_dashboard = current_dashboard_name;
|
||||
}
|
||||
|
||||
set_breadcrumbs() {
|
||||
frappe.breadcrumbs.add("Desk", "Dashboard");
|
||||
}
|
||||
|
||||
refresh() {
|
||||
frappe.run_serially([
|
||||
() => this.render_cards(),
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
}
|
||||
|
||||
add_check_events() {
|
||||
let me = this;
|
||||
this.body.on("click", ".show-user-permissions", () => {
|
||||
frappe.route_options = { allow: this.get_doctype() || "" };
|
||||
frappe.set_route('List', 'User Permission');
|
||||
|
|
@ -373,7 +374,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
// exception: reverse
|
||||
chk.prop("checked", !chk.prop("checked"));
|
||||
} else {
|
||||
this.get_perm(args.role)[args.ptype] = args.value;
|
||||
me.get_perm(args.role)[args.ptype] = args.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Elements</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
|
||||
"creation": "2021-01-02 10:51:16.579957",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-09-05 21:14:52.384816",
|
||||
"modified": "2022-01-13 17:26:02.736366",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Build",
|
||||
|
|
@ -231,7 +231,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 5,
|
||||
"sequence_id": 5.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"doc_view": "",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\":\"header\",\"data\": {\"text\":\"Settings\",\"level\": 4,\"col\": 12}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"System Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Print Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Website Settings\",\"col\": 4}}, {\"type\":\"spacer\",\"data\": {\"col\": 12}}, {\"type\":\"header\",\"data\": {\"text\":\"Reports & Masters\",\"level\": 4,\"col\": 12}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Data\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Email / Notifications\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Website\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Core\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Printing\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Workflow\",\"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Settings</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:09:40.527211",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -367,7 +367,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:03.456174",
|
||||
"modified": "2022-01-13 17:49:59.586909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Settings",
|
||||
|
|
@ -376,7 +376,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 29,
|
||||
"sequence_id": 29.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"icon": "setting",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Permission Manager\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Profile\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Type\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Users\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Logs\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Permissions\", \"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:12:16.754449",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:03.010205",
|
||||
"modified": "2022-01-13 17:49:08.912772",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 27,
|
||||
"sequence_id": 27.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "User",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ FRAPPE_EXCLUSIONS = [
|
|||
"*/commands/*",
|
||||
"*/frappe/change_log/*",
|
||||
"*/frappe/exceptions*",
|
||||
"*/frappe/coverage.py",
|
||||
"*frappe/setup.py",
|
||||
"*/doctype/*/*_dashboard.py",
|
||||
"*/patches/*",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Client Script', {
|
||||
setup(frm) {
|
||||
frm.get_field("sample").html(SAMPLE_HTML);
|
||||
},
|
||||
refresh(frm) {
|
||||
if (frm.doc.dt && frm.doc.script) {
|
||||
frm.add_custom_button(__('Go to {0}', [frm.doc.dt]),
|
||||
|
|
@ -97,3 +100,56 @@ frappe.ui.form.on('${doctype}', {
|
|||
frm.set_value('script', script + boilerplate);
|
||||
}
|
||||
});
|
||||
|
||||
const SAMPLE_HTML = `<h3>Client Script Help</h3>
|
||||
<p>Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>
|
||||
<pre><code>
|
||||
|
||||
// fetch local_tax_no on selection of customer
|
||||
// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname);
|
||||
cur_frm.add_fetch("customer", "local_tax_no', 'local_tax_no');
|
||||
|
||||
// additional validation on dates
|
||||
frappe.ui.form.on('Task', 'validate', function(frm) {
|
||||
if (frm.doc.from_date < get_today()) {
|
||||
msgprint('You can not select past date in From Date');
|
||||
validated = false;
|
||||
}
|
||||
});
|
||||
|
||||
// make a field read-only after saving
|
||||
frappe.ui.form.on('Task', {
|
||||
refresh: function(frm) {
|
||||
// use the __islocal value of doc, to check if the doc is saved or not
|
||||
frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);
|
||||
}
|
||||
});
|
||||
|
||||
// additional permission check
|
||||
frappe.ui.form.on('Task', {
|
||||
validate: function(frm) {
|
||||
if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {
|
||||
msgprint('You are only allowed Material Receipt');
|
||||
validated = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// calculate sales incentive
|
||||
frappe.ui.form.on('Sales Invoice', {
|
||||
validate: function(frm) {
|
||||
// calculate incentives for each person on the deal
|
||||
total_incentive = 0
|
||||
$.each(frm.doc.sales_team, function(i, d) {
|
||||
// calculate incentive
|
||||
var incentive_percent = 2;
|
||||
if(frm.doc.base_grand_total > 400) incentive_percent = 4;
|
||||
// actual incentive
|
||||
d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
|
||||
total_incentive += flt(d.incentives)
|
||||
});
|
||||
frm.doc.total_incentive = total_incentive;
|
||||
}
|
||||
})
|
||||
|
||||
</code></pre>`;
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@
|
|||
{
|
||||
"fieldname": "sample",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Sample",
|
||||
"options": "<h3>Client Script Help</h3>\n<p>Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>\n<pre><code>\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n</code></pre>"
|
||||
"label": "Sample"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -76,7 +75,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-04 12:03:27.029815",
|
||||
"modified": "2022-02-18 00:43:33.941466",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Client Script",
|
||||
|
|
@ -107,5 +106,6 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,458 +1,468 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-10 16:34:01",
|
||||
"description": "Adds a custom field to a DocType",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dt",
|
||||
"module",
|
||||
"label",
|
||||
"label_help",
|
||||
"fieldname",
|
||||
"insert_after",
|
||||
"length",
|
||||
"column_break_6",
|
||||
"fieldtype",
|
||||
"precision",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
"options",
|
||||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
"options_help",
|
||||
"section_break_11",
|
||||
"collapsible",
|
||||
"collapsible_depends_on",
|
||||
"default",
|
||||
"depends_on",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"properties",
|
||||
"non_negative",
|
||||
"reqd",
|
||||
"unique",
|
||||
"read_only",
|
||||
"ignore_user_permissions",
|
||||
"hidden",
|
||||
"print_hide",
|
||||
"print_hide_if_no_value",
|
||||
"print_width",
|
||||
"no_copy",
|
||||
"allow_on_submit",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"report_hide",
|
||||
"search_index",
|
||||
"allow_in_quick_entry",
|
||||
"ignore_xss_filter",
|
||||
"translatable",
|
||||
"hide_border",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
"columns"
|
||||
],
|
||||
"fields": [{
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
|
||||
"fieldname": "non_negative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Negative"
|
||||
},
|
||||
{
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for export)",
|
||||
"options": "Module Def"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-04 12:45:23.810120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "dt,label,fieldtype,options",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-10 16:34:01",
|
||||
"description": "Adds a custom field to a DocType",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dt",
|
||||
"module",
|
||||
"label",
|
||||
"label_help",
|
||||
"fieldname",
|
||||
"insert_after",
|
||||
"length",
|
||||
"column_break_6",
|
||||
"fieldtype",
|
||||
"precision",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
"options",
|
||||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
"options_help",
|
||||
"section_break_11",
|
||||
"collapsible",
|
||||
"collapsible_depends_on",
|
||||
"default",
|
||||
"depends_on",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"properties",
|
||||
"non_negative",
|
||||
"reqd",
|
||||
"unique",
|
||||
"is_virtual",
|
||||
"read_only",
|
||||
"ignore_user_permissions",
|
||||
"hidden",
|
||||
"print_hide",
|
||||
"print_hide_if_no_value",
|
||||
"print_width",
|
||||
"no_copy",
|
||||
"allow_on_submit",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"report_hide",
|
||||
"search_index",
|
||||
"allow_in_quick_entry",
|
||||
"ignore_xss_filter",
|
||||
"translatable",
|
||||
"hide_border",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
"columns"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_virtual",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Virtual"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
|
||||
"fieldname": "non_negative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Negative"
|
||||
},
|
||||
{
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for export)",
|
||||
"options": "Module Def"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-22 03:47:27.097911",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "dt,label,fieldtype,options",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ class CustomField(Document):
|
|||
old_fieldtype = self.db_get('fieldtype')
|
||||
is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)
|
||||
|
||||
if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
|
||||
if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
|
||||
frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))
|
||||
|
||||
if not self.fieldname:
|
||||
|
|
@ -65,7 +65,7 @@ class CustomField(Document):
|
|||
|
||||
if not self.flags.ignore_validate:
|
||||
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
|
||||
check_fieldname_conflicts(self.dt, self.fieldname)
|
||||
check_fieldname_conflicts(self)
|
||||
|
||||
def on_update(self):
|
||||
if not frappe.flags.in_setup_wizard:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ frappe.ui.form.on("Customize Form", {
|
|||
},
|
||||
|
||||
onload: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.set_query("doc_type", function() {
|
||||
return {
|
||||
translate_values: false,
|
||||
|
|
@ -110,7 +109,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.disable_save(true);
|
||||
frm.page.clear_icons();
|
||||
|
||||
if (frm.doc.doc_type) {
|
||||
|
|
@ -169,7 +168,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
doc_type = localStorage.getItem("customize_doctype");
|
||||
}
|
||||
if (doc_type) {
|
||||
setTimeout(() => frm.set_value("doc_type", doc_type), 1000);
|
||||
setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -341,11 +340,11 @@ frappe.customize_form.confirm = function(msg, frm) {
|
|||
}
|
||||
|
||||
frappe.customize_form.clear_locals_and_refresh = function(frm) {
|
||||
delete frm.doc.__unsaved;
|
||||
// clear doctype from locals
|
||||
frappe.model.clear_doc("DocType", frm.doc.doc_type);
|
||||
delete frappe.meta.docfield_copy[frm.doc.doc_type];
|
||||
|
||||
frm.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"autoname",
|
||||
"view_settings_section",
|
||||
"title_field",
|
||||
"show_title_field_in_link",
|
||||
"image_field",
|
||||
"default_print_format",
|
||||
"column_break_29",
|
||||
|
|
@ -296,6 +297,12 @@
|
|||
"fieldtype": "Table",
|
||||
"label": "States",
|
||||
"options": "DocType State"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_title_field_in_link",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Title in Link Fields"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -304,7 +311,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-14 16:45:04.308690",
|
||||
"modified": "2022-01-07 16:07:06.196534",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -107,20 +107,26 @@ class CustomizeForm(Document):
|
|||
def set_name_translation(self):
|
||||
'''Create, update custom translation for this doctype'''
|
||||
current = self.get_name_translation()
|
||||
if current:
|
||||
if self.label and current.translated_text != self.label:
|
||||
frappe.db.set_value('Translation', current.name, 'translated_text', self.label)
|
||||
frappe.translate.clear_cache()
|
||||
else:
|
||||
if not self.label:
|
||||
if current:
|
||||
# clear translation
|
||||
frappe.delete_doc('Translation', current.name)
|
||||
return
|
||||
|
||||
else:
|
||||
if self.label:
|
||||
frappe.get_doc(dict(doctype='Translation',
|
||||
source_text=self.doc_type,
|
||||
translated_text=self.label,
|
||||
language_code=frappe.local.lang or 'en')).insert()
|
||||
if not current:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": 'Translation',
|
||||
"source_text": self.doc_type,
|
||||
"translated_text": self.label,
|
||||
"language_code": frappe.local.lang or 'en'
|
||||
}
|
||||
).insert()
|
||||
return
|
||||
|
||||
if self.label != current.translated_text:
|
||||
frappe.db.set_value('Translation', current.name, 'translated_text', self.label)
|
||||
frappe.translate.clear_cache()
|
||||
|
||||
def clear_existing_doc(self):
|
||||
doc_type = self.doc_type
|
||||
|
|
@ -377,7 +383,7 @@ class CustomizeForm(Document):
|
|||
|
||||
def make_property_setter(self, prop, value, property_type, fieldname=None,
|
||||
apply_on=None, row_name = None):
|
||||
delete_property_setter(self.doc_type, prop, fieldname)
|
||||
delete_property_setter(self.doc_type, prop, fieldname, row_name)
|
||||
|
||||
property_value = self.get_existing_property_value(prop, fieldname)
|
||||
|
||||
|
|
@ -412,6 +418,9 @@ class CustomizeForm(Document):
|
|||
return property_value
|
||||
|
||||
def validate_fieldtype_change(self, df, old_value, new_value):
|
||||
if df.is_virtual:
|
||||
return
|
||||
|
||||
allowed = self.allow_fieldtype_change(old_value, new_value)
|
||||
if allowed:
|
||||
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
|
||||
|
|
@ -424,7 +433,8 @@ class CustomizeForm(Document):
|
|||
self.validate_fieldtype_length()
|
||||
else:
|
||||
self.flags.update_db = True
|
||||
if not allowed:
|
||||
|
||||
else:
|
||||
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
|
||||
|
||||
def validate_fieldtype_length(self):
|
||||
|
|
@ -506,7 +516,8 @@ doctype_properties = {
|
|||
'email_append_to': 'Check',
|
||||
'subject_field': 'Data',
|
||||
'sender_field': 'Data',
|
||||
'autoname': 'Data'
|
||||
'autoname': 'Data',
|
||||
'show_title_field_in_link': 'Check'
|
||||
}
|
||||
|
||||
docfield_properties = {
|
||||
|
|
@ -529,6 +540,7 @@ docfield_properties = {
|
|||
'in_global_search': 'Check',
|
||||
'in_preview': 'Check',
|
||||
'bold': 'Check',
|
||||
'no_copy': 'Check',
|
||||
'hidden': 'Check',
|
||||
'collapsible': 'Check',
|
||||
'collapsible_depends_on': 'Data',
|
||||
|
|
@ -552,7 +564,8 @@ docfield_properties = {
|
|||
'allow_in_quick_entry': 'Check',
|
||||
'hide_border': 'Check',
|
||||
'hide_days': 'Check',
|
||||
'hide_seconds': 'Check'
|
||||
'hide_seconds': 'Check',
|
||||
'is_virtual': 'Check',
|
||||
}
|
||||
|
||||
doctype_link_properties = {
|
||||
|
|
@ -587,4 +600,4 @@ ALLOWED_FIELDTYPE_CHANGE = (
|
|||
('Code', 'Geolocation'),
|
||||
('Table', 'Table MultiSelect'))
|
||||
|
||||
ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data')
|
||||
ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Data')
|
||||
|
|
|
|||
|
|
@ -97,13 +97,18 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
|
||||
custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0]
|
||||
custom_field.reqd = 1
|
||||
custom_field.no_copy = 1
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 1)
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 1)
|
||||
|
||||
custom_field = d.get("fields", {"is_custom_field": True})[0]
|
||||
custom_field.reqd = 0
|
||||
custom_field.no_copy = 0
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 0)
|
||||
|
||||
|
||||
def test_save_customization_new_field(self):
|
||||
d = self.get_customize_form("Event")
|
||||
|
|
@ -257,7 +262,7 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
frappe.clear_cache()
|
||||
d = self.get_customize_form("User Group")
|
||||
|
||||
d.append('links', dict(link_doctype='User Group Member', parent_doctype='User',
|
||||
d.append('links', dict(link_doctype='User Group Member', parent_doctype='User Group',
|
||||
link_fieldname='user', table_fieldname='user_group_members', group='Tests', custom=1))
|
||||
|
||||
d.run_method("save_customization")
|
||||
|
|
@ -267,7 +272,7 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
|
||||
# check links exist
|
||||
self.assertTrue([d.name for d in user_group.links if d.link_doctype == 'User Group Member'])
|
||||
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User'])
|
||||
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User Group'])
|
||||
|
||||
# remove the link
|
||||
d = self.get_customize_form("User Group")
|
||||
|
|
@ -304,3 +309,25 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
|
||||
action = [d for d in event.actions if d.label=='Test Action']
|
||||
self.assertEqual(len(action), 0)
|
||||
|
||||
def test_custom_label(self):
|
||||
d = self.get_customize_form("Event")
|
||||
|
||||
# add label
|
||||
d.label = "Test Rename"
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "Test Rename")
|
||||
|
||||
# change label
|
||||
d.label = "Test Rename 2"
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "Test Rename 2")
|
||||
|
||||
# saving again to make sure existing label persists
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "Test Rename 2")
|
||||
|
||||
# clear label
|
||||
d.label = ""
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue