Merge branch 'develop' into frm_call
This commit is contained in:
commit
87fb4d4459
90 changed files with 2527 additions and 1310 deletions
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"')
|
||||
|
|
|
|||
16
.github/workflows/ui-tests.yml
vendored
16
.github/workflows/ui-tests.yml
vendored
|
|
@ -141,6 +141,12 @@ jobs:
|
|||
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 }}
|
||||
|
|
|
|||
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
|
||||
};
|
||||
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
|
||||
};
|
||||
46
cypress/fixtures/doctype_with_child_table.js
Normal file
46
cypress/fixtures/doctype_with_child_table.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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
|
||||
}
|
||||
],
|
||||
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
|
||||
};
|
||||
|
|
@ -95,6 +95,51 @@ context('Control Link', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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]}`);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
|
||||
import child_table_doctype from '../fixtures/child_table_doctype';
|
||||
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', 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 +76,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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -11,30 +11,63 @@ context('Report View', () => {
|
|||
'title': 'Doc 1',
|
||||
'description': 'Random Text',
|
||||
'enabled': 0,
|
||||
// submit document
|
||||
'docstatus': 1
|
||||
}, true).as('doc');
|
||||
'docstatus': 1 // submit document
|
||||
}, true);
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records");
|
||||
});
|
||||
});
|
||||
|
||||
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.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);
|
||||
});
|
||||
});
|
||||
|
||||
it('test load more with count selection buttons', () => {
|
||||
cy.visit('/app/contact/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');
|
||||
});
|
||||
});
|
||||
|
|
@ -159,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():
|
||||
|
|
@ -208,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
|
||||
|
|
|
|||
|
|
@ -89,6 +89,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
|
||||
|
||||
|
|
@ -324,6 +325,15 @@ def get_desk_settings():
|
|||
def get_notification_settings():
|
||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
|
||||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -447,21 +447,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
|
||||
|
||||
|
|
|
|||
|
|
@ -742,8 +742,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
|
||||
|
||||
|
|
@ -751,8 +752,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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -367,15 +368,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 +398,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 +454,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from urllib.parse import quote
|
|||
|
||||
import frappe
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
from frappe.core.doctype.communication.communication import get_emails
|
||||
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
|
||||
|
|
@ -201,6 +202,19 @@ class TestCommunication(unittest.TestCase):
|
|||
|
||||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
||||
def 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 [])
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"hide_days",
|
||||
"hide_seconds",
|
||||
"reqd",
|
||||
"is_virtual",
|
||||
"search_index",
|
||||
"column_break_18",
|
||||
"options",
|
||||
|
|
@ -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-01-27 21:22:20.529072",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"allow_auto_repeat",
|
||||
"view_settings",
|
||||
"title_field",
|
||||
"show_title_field_in_link",
|
||||
"search_fields",
|
||||
"default_print_format",
|
||||
"sort_field",
|
||||
|
|
@ -582,6 +583,12 @@
|
|||
"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",
|
||||
|
|
@ -663,7 +670,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2021-12-09 14:53:10.717788",
|
||||
"modified": "2022-01-07 16:07:06.196534",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -781,29 +781,31 @@ 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):
|
||||
fieldnames = tuple(field.fieldname for field in meta.fields)
|
||||
for index, link in enumerate(meta.links, 1):
|
||||
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))
|
||||
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 link.table_fieldname not in fieldnames:
|
||||
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)
|
||||
|
|
@ -1076,6 +1078,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))
|
||||
|
|
@ -1321,10 +1326,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)
|
||||
|
|
@ -1332,9 +1336,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')
|
||||
|
|
|
|||
|
|
@ -745,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":
|
||||
|
|
|
|||
|
|
@ -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,55 @@ 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, report_settings=report_settings)
|
||||
self.assertEqual(result[-1][0], "Total")
|
||||
self.assertEqual(result[-1][1], 200)
|
||||
self.assertEqual(result[-1][2], 150.50)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ FRAPPE_EXCLUSIONS = [
|
|||
"*/commands/*",
|
||||
"*/frappe/change_log/*",
|
||||
"*/frappe/exceptions*",
|
||||
"*/frappe/coverage.py",
|
||||
"*frappe/setup.py",
|
||||
"*/doctype/*/*_dashboard.py",
|
||||
"*/patches/*",
|
||||
|
|
|
|||
|
|
@ -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\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": "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\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-01-27 21:47:01.065556",
|
||||
"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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -418,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])
|
||||
|
|
@ -430,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):
|
||||
|
|
@ -512,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 = {
|
||||
|
|
@ -558,7 +563,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 = {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"non_negative",
|
||||
"reqd",
|
||||
"unique",
|
||||
"is_virtual",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
|
|
@ -115,6 +116,12 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_virtual",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Virtual"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
|
|
@ -436,7 +443,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-03 14:50:32.035768",
|
||||
"modified": "2022-01-27 21:45:22.349776",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
import frappe
|
||||
|
|
@ -18,53 +18,19 @@ class PropertySetter(Document):
|
|||
|
||||
def validate(self):
|
||||
self.validate_fieldtype_change()
|
||||
|
||||
if self.is_new():
|
||||
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
|
||||
|
||||
# clear cache
|
||||
frappe.clear_cache(doctype = self.doc_type)
|
||||
|
||||
def validate_fieldtype_change(self):
|
||||
if self.field_name in not_allowed_fieldtype_change and \
|
||||
self.property == 'fieldtype':
|
||||
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
|
||||
|
||||
def get_property_list(self, dt):
|
||||
return frappe.db.get_all('DocField',
|
||||
fields=['fieldname', 'label', 'fieldtype'],
|
||||
filters={
|
||||
'parent': dt,
|
||||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
|
||||
'fieldname': ['!=', '']
|
||||
},
|
||||
order_by='label asc',
|
||||
as_dict=1
|
||||
)
|
||||
|
||||
def get_setup_data(self):
|
||||
return {
|
||||
'doctypes': frappe.get_all("DocType", pluck="name"),
|
||||
'dt_properties': self.get_property_list('DocType'),
|
||||
'df_properties': self.get_property_list('DocField')
|
||||
}
|
||||
|
||||
def get_field_ids(self):
|
||||
return frappe.db.get_values(
|
||||
"DocField",
|
||||
filters={"parent": self.doc_type},
|
||||
fieldname=["name", "fieldtype", "label", "fieldname"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
def get_defaults(self):
|
||||
if not self.field_name:
|
||||
return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
|
||||
else:
|
||||
return frappe.db.get_values(
|
||||
"DocField",
|
||||
filters={"fieldname": self.field_name, "parent": self.doc_type},
|
||||
fieldname="*",
|
||||
)[0]
|
||||
if (
|
||||
self.property == 'fieldtype'
|
||||
and self.field_name in not_allowed_fieldtype_change
|
||||
):
|
||||
frappe.throw(
|
||||
_("Field type cannot be changed for {0}").format(self.field_name)
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
if frappe.flags.in_patch:
|
||||
|
|
@ -74,6 +40,7 @@ class PropertySetter(Document):
|
|||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
|
||||
validate_fields_for_doctype(self.doc_type)
|
||||
|
||||
|
||||
def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
|
||||
validate_fields_for_doctype=True):
|
||||
# WARNING: Ignores Permissions
|
||||
|
|
@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
|
|||
property_setter.insert()
|
||||
return property_setter
|
||||
|
||||
|
||||
def delete_property_setter(doc_type, property, field_name=None, row_name=None):
|
||||
"""delete other property setters on this, if this is new"""
|
||||
filters = dict(doc_type=doc_type, property=property)
|
||||
|
|
@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None):
|
|||
filters["row_name"] = row_name
|
||||
|
||||
frappe.db.delete('Property Setter', filters)
|
||||
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@ class Database(object):
|
|||
raise frappe.QueryTimeoutError(e)
|
||||
|
||||
elif frappe.conf.db_type == 'postgres':
|
||||
# TODO: added temporarily
|
||||
print(e)
|
||||
raise
|
||||
|
||||
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ CREATE TABLE `tabDocType` (
|
|||
`email_append_to` int(1) NOT NULL DEFAULT 0,
|
||||
`subject_field` varchar(255) DEFAULT NULL,
|
||||
`sender_field` varchar(255) DEFAULT NULL,
|
||||
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
|
||||
`migration_hash` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`name`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ CREATE TABLE "tabDocType" (
|
|||
"email_append_to" smallint NOT NULL DEFAULT 0,
|
||||
"subject_field" varchar(255) DEFAULT NULL,
|
||||
"sender_field" varchar(255) DEFAULT NULL,
|
||||
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
|
||||
"migration_hash" varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY ("name")
|
||||
) ;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class DBTable:
|
|||
"""
|
||||
get columns from docfields and custom fields
|
||||
"""
|
||||
fields = self.meta.get_fieldnames_with_value(True)
|
||||
fields = self.meta.get_fieldnames_with_value(with_field_meta=True)
|
||||
|
||||
# optional fields like _comments
|
||||
if not self.meta.get('istable'):
|
||||
|
|
@ -85,6 +85,9 @@ class DBTable:
|
|||
})
|
||||
|
||||
for field in fields:
|
||||
if field.get("is_virtual"):
|
||||
continue
|
||||
|
||||
self.columns[field.get('fieldname')] = DbColumn(
|
||||
self,
|
||||
field.get('fieldname'),
|
||||
|
|
|
|||
16
frappe/desk/doctype/dashboard/dashboard_list.js
Normal file
16
frappe/desk/doctype/dashboard/dashboard_list.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
frappe.listview_settings['Dashboard'] = {
|
||||
button: {
|
||||
show(doc) {
|
||||
return doc.name;
|
||||
},
|
||||
get_label() {
|
||||
return frappe.utils.icon("dashboard-list", "sm");
|
||||
},
|
||||
get_description(doc) {
|
||||
return __('View {0}', [`${doc.name}`]);
|
||||
},
|
||||
action(doc) {
|
||||
frappe.set_route('dashboard-view', doc.name);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -49,7 +49,7 @@ def getdoc(doctype, name, user=None):
|
|||
raise
|
||||
|
||||
doc.add_seen()
|
||||
|
||||
set_link_titles(doc)
|
||||
frappe.response.docs.append(doc)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -367,6 +367,60 @@ def get_additional_timeline_content(doctype, docname):
|
|||
|
||||
return contents
|
||||
|
||||
def set_link_titles(doc):
|
||||
link_titles = {}
|
||||
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc))
|
||||
link_titles.update(get_title_values_for_table_and_multiselect_fields(doc))
|
||||
|
||||
send_link_titles(link_titles)
|
||||
|
||||
def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None):
|
||||
link_titles = {}
|
||||
|
||||
if not link_fields:
|
||||
meta = frappe.get_meta(doc.doctype)
|
||||
link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields()
|
||||
|
||||
for field in link_fields:
|
||||
if not doc.get(field.fieldname):
|
||||
continue
|
||||
|
||||
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
if not meta or not (meta.title_field and meta.show_title_field_in_link):
|
||||
continue
|
||||
|
||||
link_title = frappe.db.get_value(
|
||||
doctype, doc.get(field.fieldname), meta.title_field, cache=True
|
||||
)
|
||||
link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title})
|
||||
|
||||
return link_titles
|
||||
|
||||
def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None):
|
||||
link_titles = {}
|
||||
|
||||
if not table_fields:
|
||||
meta = frappe.get_meta(doc.doctype)
|
||||
table_fields = meta.get_table_fields()
|
||||
|
||||
for field in table_fields:
|
||||
if not doc.get(field.fieldname):
|
||||
continue
|
||||
|
||||
for value in doc.get(field.fieldname):
|
||||
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value))
|
||||
|
||||
return link_titles
|
||||
|
||||
def send_link_titles(link_titles):
|
||||
"""Append link titles dict in `frappe.local.response`."""
|
||||
if "_link_titles" not in frappe.local.response:
|
||||
frappe.local.response["_link_titles"] = {}
|
||||
|
||||
frappe.local.response["_link_titles"].update(link_titles)
|
||||
|
||||
def update_user_info(docinfo):
|
||||
for d in docinfo.communications:
|
||||
frappe.utils.add_user_info(d.sender, docinfo.user_info)
|
||||
|
|
@ -387,3 +441,4 @@ def get_user_info_for_viewers(users):
|
|||
frappe.utils.add_user_info(user, user_info)
|
||||
|
||||
return user_info
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def get_report_result(report, filters):
|
|||
return res
|
||||
|
||||
@frappe.read_only()
|
||||
def generate_report_result(report, filters=None, user=None, custom_columns=None):
|
||||
def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None):
|
||||
user = user or frappe.session.user
|
||||
filters = filters or []
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
|
|||
result = get_filtered_data(report.ref_doctype, columns, result, user)
|
||||
|
||||
if cint(report.add_total_row) and result and not skip_total_row:
|
||||
result = add_total_row(result, columns)
|
||||
result = add_total_row(result, columns, report_settings=report_settings)
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
|
|
@ -210,7 +210,7 @@ def get_script(report_name):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
|
||||
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None):
|
||||
report = get_report_doc(report_name)
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
|
@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
|
|||
dn = ""
|
||||
result = get_prepared_report_result(report, filters, dn, user)
|
||||
else:
|
||||
result = generate_report_result(report, filters, user, custom_columns)
|
||||
result = generate_report_result(report, filters, user, custom_columns, report_settings)
|
||||
|
||||
result["add_total_row"] = report.add_total_row and not result.get(
|
||||
"skip_total_row", False
|
||||
|
|
@ -435,9 +435,19 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
|
|||
return result, column_widths
|
||||
|
||||
|
||||
def add_total_row(result, columns, meta=None):
|
||||
def add_total_row(result, columns, meta=None, report_settings=None):
|
||||
total_row = [""] * len(columns)
|
||||
has_percent = []
|
||||
is_tree = False
|
||||
parent_field = ''
|
||||
|
||||
if report_settings:
|
||||
if isinstance(report_settings, (str,)):
|
||||
report_settings = json.loads(report_settings)
|
||||
|
||||
is_tree = report_settings.get('tree')
|
||||
parent_field = report_settings.get('parent_field')
|
||||
|
||||
for i, col in enumerate(columns):
|
||||
fieldtype, options, fieldname = None, None, None
|
||||
if isinstance(col, str):
|
||||
|
|
@ -464,12 +474,12 @@ def add_total_row(result, columns, meta=None):
|
|||
for row in result:
|
||||
if i >= len(row):
|
||||
continue
|
||||
|
||||
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
|
||||
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(
|
||||
cell
|
||||
):
|
||||
total_row[i] = flt(total_row[i]) + flt(cell)
|
||||
if not (is_tree and row.get(parent_field)):
|
||||
total_row[i] = flt(total_row[i]) + flt(cell)
|
||||
|
||||
if fieldtype == "Percent" and i not in has_percent:
|
||||
has_percent.append(i)
|
||||
|
|
|
|||
|
|
@ -262,22 +262,66 @@ def compress(data, args=None):
|
|||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_report():
|
||||
"""save report"""
|
||||
def save_report(name, doctype, report_settings):
|
||||
"""Save reports of type Report Builder from Report View"""
|
||||
|
||||
data = frappe.local.form_dict
|
||||
if frappe.db.exists('Report', data['name']):
|
||||
d = frappe.get_doc('Report', data['name'])
|
||||
if frappe.db.exists('Report', name):
|
||||
report = frappe.get_doc('Report', name)
|
||||
if report.is_standard == "Yes":
|
||||
frappe.throw(_("Standard Reports cannot be edited"))
|
||||
|
||||
if report.report_type != "Report Builder":
|
||||
frappe.throw(_("Only reports of type Report Builder can be edited"))
|
||||
|
||||
if (
|
||||
report.owner != frappe.session.user
|
||||
and not frappe.has_permission("Report", "write")
|
||||
):
|
||||
frappe.throw(
|
||||
_("Insufficient Permissions for editing Report"),
|
||||
frappe.PermissionError
|
||||
)
|
||||
else:
|
||||
d = frappe.new_doc('Report')
|
||||
d.report_name = data['name']
|
||||
d.ref_doctype = data['doctype']
|
||||
report = frappe.new_doc('Report')
|
||||
report.report_name = name
|
||||
report.ref_doctype = doctype
|
||||
|
||||
d.report_type = "Report Builder"
|
||||
d.json = data['json']
|
||||
frappe.get_doc(d).save()
|
||||
frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
|
||||
return d.name
|
||||
report.report_type = "Report Builder"
|
||||
report.json = report_settings
|
||||
report.save(ignore_permissions=True)
|
||||
frappe.msgprint(
|
||||
_("Report {0} saved").format(frappe.bold(report.name)),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
return report.name
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_report(name):
|
||||
"""Delete reports of type Report Builder from Report View"""
|
||||
|
||||
report = frappe.get_doc("Report", name)
|
||||
if report.is_standard == "Yes":
|
||||
frappe.throw(_("Standard Reports cannot be deleted"))
|
||||
|
||||
if report.report_type != "Report Builder":
|
||||
frappe.throw(_("Only reports of type Report Builder can be deleted"))
|
||||
|
||||
if (
|
||||
report.owner != frappe.session.user
|
||||
and not frappe.has_permission("Report", "delete")
|
||||
):
|
||||
frappe.throw(
|
||||
_("Insufficient Permissions for deleting Report"),
|
||||
frappe.PermissionError
|
||||
)
|
||||
|
||||
report.delete(ignore_permissions=True)
|
||||
frappe.msgprint(
|
||||
_("Report {0} deleted").format(frappe.bold(report.name)),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ def sanitize_searchfield(searchfield):
|
|||
# this is called by the Link Field
|
||||
@frappe.whitelist()
|
||||
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
|
||||
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
|
||||
frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
|
||||
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters,
|
||||
reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
|
||||
|
||||
frappe.response["results"] = build_for_autosuggest(frappe.response["values"], doctype=doctype)
|
||||
del frappe.response["values"]
|
||||
|
||||
# this is called by the search box
|
||||
|
|
@ -138,6 +140,12 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
fields = list(set(fields + json.loads(filter_fields)))
|
||||
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]
|
||||
|
||||
title_field_query = get_title_field_query(meta)
|
||||
|
||||
# Insert title field query after name
|
||||
if title_field_query:
|
||||
formatted_fields.insert(1, title_field_query)
|
||||
|
||||
# find relevance as location of search term from the beginning of string `name`. used for sorting results.
|
||||
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
|
||||
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype))
|
||||
|
|
@ -205,11 +213,38 @@ def get_std_fields_list(meta, key):
|
|||
|
||||
return sflist
|
||||
|
||||
def build_for_autosuggest(res):
|
||||
def get_title_field_query(meta):
|
||||
title_field = meta.title_field if meta.title_field else None
|
||||
show_title_field_in_link = meta.show_title_field_in_link if meta.show_title_field_in_link else None
|
||||
field = None
|
||||
|
||||
if title_field and show_title_field_in_link:
|
||||
field = "`tab{0}`.{1} as `label`".format(meta.name, title_field)
|
||||
|
||||
return field
|
||||
|
||||
def build_for_autosuggest(res, doctype):
|
||||
results = []
|
||||
for r in res:
|
||||
out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
|
||||
results.append(out)
|
||||
meta = frappe.get_meta(doctype)
|
||||
if not (meta.title_field and meta.show_title_field_in_link):
|
||||
for r in res:
|
||||
r = list(r)
|
||||
results.append({
|
||||
"value": r[0],
|
||||
"description": ", ".join(unique(cstr(d) for d in r[1:] if d))
|
||||
})
|
||||
|
||||
else:
|
||||
title_field_exists = meta.title_field and meta.show_title_field_in_link
|
||||
_from = 2 if title_field_exists else 1 # to exclude title from description if title_field_exists
|
||||
for r in res:
|
||||
r = list(r)
|
||||
results.append({
|
||||
"value": r[0],
|
||||
"label": r[1] if title_field_exists else None,
|
||||
"description": ", ".join(unique(cstr(d) for d in r[_from:] if d))
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def scrub_custom_query(query, key, txt):
|
||||
|
|
@ -272,3 +307,12 @@ def get_user_groups():
|
|||
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
|
||||
'is_group': True
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_link_title(doctype, docname):
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if meta.title_field and meta.show_title_field_in_link:
|
||||
return frappe.db.get_value(doctype, docname, meta.title_field)
|
||||
|
||||
return docname
|
||||
|
|
@ -529,10 +529,9 @@ def extract_sql_gzip(sql_gz_path):
|
|||
import subprocess
|
||||
|
||||
try:
|
||||
# dvf - decompress, verbose, force
|
||||
original_file = sql_gz_path
|
||||
decompressed_file = original_file.rstrip(".gz")
|
||||
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
|
||||
cmd = 'gzip --decompress --force < {0} > {1}'.format(original_file, decompressed_file)
|
||||
subprocess.check_call(cmd, shell=True)
|
||||
except Exception:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -1,30 +1,54 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
|
||||
import frappe
|
||||
import frappe.translate
|
||||
import frappe.modules.patch_handler
|
||||
import frappe.model.sync
|
||||
from frappe.utils.fixtures import sync_fixtures
|
||||
import frappe.modules.patch_handler
|
||||
import frappe.translate
|
||||
from frappe.cache_manager import clear_global_cache
|
||||
from frappe.core.doctype.language.language import sync_languages
|
||||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
|
||||
from frappe.database.schema import add_column
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
from frappe.modules.patch_handler import PatchType
|
||||
from frappe.modules.utils import sync_customizations
|
||||
from frappe.search.website_search import build_index_for_all_routes
|
||||
from frappe.utils.connections import check_connection
|
||||
from frappe.utils.dashboard import sync_dashboards
|
||||
from frappe.cache_manager import clear_global_cache
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
from frappe.utils.fixtures import sync_fixtures
|
||||
from frappe.website.utils import clear_website_cache
|
||||
from frappe.core.doctype.language.language import sync_languages
|
||||
from frappe.modules.utils import sync_customizations
|
||||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
|
||||
from frappe.search.website_search import build_index_for_all_routes
|
||||
from frappe.database.schema import add_column
|
||||
from frappe.modules.patch_handler import PatchType
|
||||
|
||||
BENCH_START_MESSAGE = dedent(
|
||||
"""
|
||||
Cannot run bench migrate without the services running.
|
||||
If you are running bench in development mode, make sure that bench is running:
|
||||
|
||||
$ bench start
|
||||
|
||||
Otherwise, check the server logs and ensure that all the required services are running.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def atomic(method):
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
ret = method(*args, **kwargs)
|
||||
frappe.db.commit()
|
||||
return ret
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
raise
|
||||
|
||||
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
|
||||
'''Migrate all apps to the current version, will:
|
||||
return wrapper
|
||||
|
||||
|
||||
class SiteMigration:
|
||||
"""Migrate all apps to the current version, will:
|
||||
- run before migrate hooks
|
||||
- run patches
|
||||
- sync doctypes (schema)
|
||||
|
|
@ -35,70 +59,117 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False):
|
|||
- sync languages
|
||||
- sync web pages (from /www)
|
||||
- run after migrate hooks
|
||||
'''
|
||||
"""
|
||||
|
||||
service_status = check_connection(redis_services=["redis_cache"])
|
||||
if False in service_status.values():
|
||||
for service in service_status:
|
||||
if not service_status.get(service, True):
|
||||
print("{} service is not running.".format(service))
|
||||
print("""Cannot run bench migrate without the services running.
|
||||
If you are running bench in development mode, make sure that bench is running:
|
||||
def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None:
|
||||
self.skip_failing = skip_failing
|
||||
self.skip_search_index = skip_search_index
|
||||
|
||||
$ bench start
|
||||
|
||||
Otherwise, check the server logs and ensure that all the required services are running.""")
|
||||
sys.exit(1)
|
||||
|
||||
touched_tables_file = frappe.get_site_path('touched_tables.json')
|
||||
if os.path.exists(touched_tables_file):
|
||||
os.remove(touched_tables_file)
|
||||
|
||||
try:
|
||||
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
|
||||
def setUp(self):
|
||||
"""Complete setup required for site migration
|
||||
"""
|
||||
frappe.flags.touched_tables = set()
|
||||
frappe.flags.in_migrate = True
|
||||
|
||||
self.touched_tables_file = frappe.get_site_path("touched_tables.json")
|
||||
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
|
||||
clear_global_cache()
|
||||
|
||||
if os.path.exists(self.touched_tables_file):
|
||||
os.remove(self.touched_tables_file)
|
||||
|
||||
frappe.flags.in_migrate = True
|
||||
|
||||
def tearDown(self):
|
||||
"""Run operations that should be run post schema updation processes
|
||||
This should be executed irrespective of outcome
|
||||
"""
|
||||
frappe.translate.clear_cache()
|
||||
clear_website_cache()
|
||||
clear_notifications()
|
||||
|
||||
with open(self.touched_tables_file, "w") as f:
|
||||
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
|
||||
|
||||
if not self.skip_search_index:
|
||||
print(f"Building search index for {frappe.local.site}")
|
||||
build_index_for_all_routes()
|
||||
|
||||
frappe.publish_realtime("version-update")
|
||||
frappe.flags.touched_tables.clear()
|
||||
frappe.flags.in_migrate = False
|
||||
|
||||
@atomic
|
||||
def pre_schema_updates(self):
|
||||
"""Executes `before_migrate` hooks
|
||||
"""
|
||||
for app in frappe.get_installed_apps():
|
||||
for fn in frappe.get_hooks('before_migrate', app_name=app):
|
||||
for fn in frappe.get_hooks("before_migrate", app_name=app):
|
||||
frappe.get_attr(fn)()
|
||||
|
||||
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync)
|
||||
@atomic
|
||||
def run_schema_updates(self):
|
||||
"""Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files
|
||||
"""
|
||||
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync)
|
||||
frappe.model.sync.sync_all()
|
||||
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync)
|
||||
frappe.translate.clear_cache()
|
||||
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync)
|
||||
|
||||
@atomic
|
||||
def post_schema_updates(self):
|
||||
"""Execute pending migration tasks post patches execution & schema sync
|
||||
This includes:
|
||||
* Sync `Scheduled Job Type` and scheduler events defined in hooks
|
||||
* Sync fixtures & custom scripts
|
||||
* Sync in-Desk Module Dashboards
|
||||
* Sync customizations: Custom Fields, Property Setters, Custom Permissions
|
||||
* Sync Frappe's internal language master
|
||||
* Sync Portal Menu Items
|
||||
* Sync Installed Applications Version History
|
||||
* Execute `after_migrate` hooks
|
||||
"""
|
||||
sync_jobs()
|
||||
sync_fixtures()
|
||||
sync_dashboards()
|
||||
sync_customizations()
|
||||
sync_languages()
|
||||
|
||||
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
|
||||
|
||||
# syncs static files
|
||||
clear_website_cache()
|
||||
|
||||
# updating installed applications data
|
||||
frappe.get_single('Installed Applications').update_versions()
|
||||
frappe.get_single("Portal Settings").sync_menu()
|
||||
frappe.get_single("Installed Applications").update_versions()
|
||||
|
||||
for app in frappe.get_installed_apps():
|
||||
for fn in frappe.get_hooks('after_migrate', app_name=app):
|
||||
for fn in frappe.get_hooks("after_migrate", app_name=app):
|
||||
frappe.get_attr(fn)()
|
||||
|
||||
if not skip_search_index:
|
||||
# Run this last as it updates the current session
|
||||
print('Building search index for {}'.format(frappe.local.site))
|
||||
build_index_for_all_routes()
|
||||
def required_services_running(self) -> bool:
|
||||
"""Returns True if all required services are running. Returns False and prints
|
||||
instructions to stdout when required services are not available.
|
||||
"""
|
||||
service_status = check_connection(redis_services=["redis_cache"])
|
||||
are_services_running = all(service_status.values())
|
||||
|
||||
frappe.db.commit()
|
||||
if not are_services_running:
|
||||
for service in service_status:
|
||||
if not service_status.get(service, True):
|
||||
print(f"Service {service} is not running.")
|
||||
print(BENCH_START_MESSAGE)
|
||||
|
||||
clear_notifications()
|
||||
return are_services_running
|
||||
|
||||
frappe.publish_realtime("version-update")
|
||||
frappe.flags.in_migrate = False
|
||||
finally:
|
||||
with open(touched_tables_file, 'w') as f:
|
||||
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
|
||||
frappe.flags.touched_tables.clear()
|
||||
def run(self, site: str):
|
||||
"""Run Migrate operation on site specified. This method initializes
|
||||
and destroys connections to the site database.
|
||||
"""
|
||||
if not self.required_services_running():
|
||||
raise SystemExit(1)
|
||||
|
||||
if site:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
|
||||
self.setUp()
|
||||
try:
|
||||
self.pre_schema_updates()
|
||||
self.run_schema_updates()
|
||||
finally:
|
||||
self.post_schema_updates()
|
||||
self.tearDown()
|
||||
frappe.destroy()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import datetime
|
||||
|
||||
import frappe
|
||||
import datetime
|
||||
from frappe import _
|
||||
from frappe.model import default_fields, table_fields, child_table_fields
|
||||
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
|
||||
from frappe.model.naming import set_new_name
|
||||
from frappe.model.utils.link_count import notify_link_count
|
||||
from frappe.modules import load_doctype_module
|
||||
from frappe.model import display_fieldtypes
|
||||
from frappe.utils import (cint, flt, now, cstr, strip_html,
|
||||
sanitize_html, sanitize_email, cast_fieldtype)
|
||||
from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html
|
||||
from frappe.utils.html_utils import unescape_html
|
||||
from frappe.model.docstatus import DocStatus
|
||||
|
||||
|
|
@ -35,13 +33,12 @@ def get_controller(doctype):
|
|||
|
||||
module_name, custom = frappe.db.get_value(
|
||||
"DocType", doctype, ("module", "custom"), cache=True
|
||||
) or ["Core", False]
|
||||
) or ("Core", False)
|
||||
|
||||
if custom:
|
||||
if frappe.db.field_exists("DocType", "is_tree"):
|
||||
is_tree = frappe.db.get_value("DocType", doctype, "is_tree", cache=True)
|
||||
else:
|
||||
is_tree = False
|
||||
is_tree = frappe.db.get_value(
|
||||
"DocType", doctype, "is_tree", ignore=True, cache=True
|
||||
)
|
||||
_class = NestedSet if is_tree else Document
|
||||
else:
|
||||
class_overrides = frappe.get_hooks('override_doctype_class')
|
||||
|
|
@ -75,9 +72,12 @@ def get_controller(doctype):
|
|||
return site_controllers[doctype]
|
||||
|
||||
class BaseDocument(object):
|
||||
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
|
||||
ignore_in_setter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
|
||||
|
||||
def __init__(self, d):
|
||||
if d.get("doctype"):
|
||||
self.doctype = d["doctype"]
|
||||
|
||||
self.update(d)
|
||||
self.dont_update_if_missing = []
|
||||
|
||||
|
|
@ -105,10 +105,9 @@ class BaseDocument(object):
|
|||
})
|
||||
"""
|
||||
|
||||
# first set default field values of base document
|
||||
for key in default_fields:
|
||||
if key in d:
|
||||
self.set(key, d[key])
|
||||
# set name first, as it is used a reference in child document
|
||||
if "name" in d:
|
||||
self.name = d["name"]
|
||||
|
||||
for key, value in d.items():
|
||||
self.set(key, value)
|
||||
|
|
@ -143,10 +142,14 @@ class BaseDocument(object):
|
|||
else:
|
||||
value = self.__dict__.get(key, default)
|
||||
|
||||
if value is None and key not in self.ignore_in_getter \
|
||||
and key in (d.fieldname for d in self.meta.get_table_fields()):
|
||||
self.set(key, [])
|
||||
value = self.__dict__.get(key)
|
||||
if value is None and key in (
|
||||
d.fieldname for d in self.meta.get_table_fields()
|
||||
):
|
||||
value = []
|
||||
self.set(key, value)
|
||||
|
||||
if limit and isinstance(value, (list, tuple)) and len(value) > limit:
|
||||
value = value[:limit]
|
||||
|
||||
return value
|
||||
else:
|
||||
|
|
@ -156,6 +159,9 @@ class BaseDocument(object):
|
|||
return self.get(key, filters=filters, limit=1)[0]
|
||||
|
||||
def set(self, key, value, as_value=False):
|
||||
if key in self.ignore_in_setter:
|
||||
return
|
||||
|
||||
if isinstance(value, list) and not as_value:
|
||||
self.__dict__[key] = []
|
||||
self.extend(key, value)
|
||||
|
|
@ -181,6 +187,7 @@ class BaseDocument(object):
|
|||
if isinstance(value, (dict, BaseDocument)):
|
||||
if not self.__dict__.get(key):
|
||||
self.__dict__[key] = []
|
||||
|
||||
value = self._init_child(value, key)
|
||||
self.__dict__[key].append(value)
|
||||
|
||||
|
|
@ -217,11 +224,11 @@ class BaseDocument(object):
|
|||
def _init_child(self, value, key):
|
||||
if not self.doctype:
|
||||
return value
|
||||
|
||||
if not isinstance(value, BaseDocument):
|
||||
if "doctype" not in value or value['doctype'] is None:
|
||||
value["doctype"] = self.get_table_field_doctype(key)
|
||||
if not value["doctype"]:
|
||||
raise AttributeError(key)
|
||||
value["doctype"] = self.get_table_field_doctype(key)
|
||||
if not value["doctype"]:
|
||||
raise AttributeError(key)
|
||||
|
||||
value = get_controller(value["doctype"])(value)
|
||||
value.init_valid_columns()
|
||||
|
|
@ -241,7 +248,7 @@ class BaseDocument(object):
|
|||
|
||||
return value
|
||||
|
||||
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False):
|
||||
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False):
|
||||
d = frappe._dict()
|
||||
for fieldname in self.meta.get_valid_columns():
|
||||
d[fieldname] = self.get(fieldname)
|
||||
|
|
@ -251,7 +258,26 @@ class BaseDocument(object):
|
|||
continue
|
||||
|
||||
df = self.meta.get_field(fieldname)
|
||||
if df:
|
||||
|
||||
if df and df.get("is_virtual"):
|
||||
if ignore_virtual:
|
||||
del d[fieldname]
|
||||
continue
|
||||
|
||||
from frappe.utils.safe_exec import get_safe_globals
|
||||
|
||||
if d[fieldname] is None:
|
||||
if df.get("options"):
|
||||
d[fieldname] = frappe.safe_eval(
|
||||
code=df.get("options"),
|
||||
eval_globals=get_safe_globals(),
|
||||
eval_locals={"doc": self},
|
||||
)
|
||||
else:
|
||||
_val = getattr(self, fieldname, None)
|
||||
if _val and not callable(_val):
|
||||
d[fieldname] = _val
|
||||
elif df:
|
||||
if df.fieldtype=="Check":
|
||||
d[fieldname] = 1 if cint(d[fieldname]) else 0
|
||||
|
||||
|
|
@ -325,6 +351,7 @@ class BaseDocument(object):
|
|||
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False):
|
||||
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
|
||||
doc["doctype"] = self.doctype
|
||||
|
||||
for df in self.meta.get_table_fields():
|
||||
children = self.get(df.fieldname) or []
|
||||
doc[df.fieldname] = [
|
||||
|
|
@ -372,26 +399,43 @@ class BaseDocument(object):
|
|||
fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
|
||||
return fieldname[0] if fieldname else None
|
||||
|
||||
def db_insert(self):
|
||||
"""INSERT the document (with valid columns) in the database."""
|
||||
def db_insert(self, ignore_if_duplicate=False):
|
||||
"""INSERT the document (with valid columns) in the database.
|
||||
|
||||
args:
|
||||
ignore_if_duplicate: ignore primary key collision
|
||||
at database level (postgres)
|
||||
in python (mariadb)
|
||||
"""
|
||||
if not self.name:
|
||||
# name will be set by document class in most cases
|
||||
set_new_name(self)
|
||||
|
||||
conflict_handler = ""
|
||||
# On postgres we can't implcitly ignore PK collision
|
||||
# So instruct pg to ignore `name` field conflicts
|
||||
if ignore_if_duplicate and frappe.db.db_type == "postgres":
|
||||
conflict_handler = "on conflict (name) do nothing"
|
||||
|
||||
if not self.creation:
|
||||
self.creation = self.modified = now()
|
||||
self.created_by = self.modified_by = frappe.session.user
|
||||
|
||||
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
|
||||
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
|
||||
d = self.get_valid_dict(
|
||||
convert_dates_to_str=True,
|
||||
ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE,
|
||||
ignore_virtual=True,
|
||||
)
|
||||
|
||||
columns = list(d)
|
||||
try:
|
||||
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
|
||||
VALUES ({values})""".format(
|
||||
doctype = self.doctype,
|
||||
columns = ", ".join("`"+c+"`" for c in columns),
|
||||
values = ", ".join(["%s"] * len(columns))
|
||||
VALUES ({values}) {conflict_handler}""".format(
|
||||
doctype=self.doctype,
|
||||
columns=", ".join("`"+c+"`" for c in columns),
|
||||
values=", ".join(["%s"] * len(columns)),
|
||||
conflict_handler=conflict_handler
|
||||
), list(d.values()))
|
||||
except Exception as e:
|
||||
if frappe.db.is_primary_key_violation(e):
|
||||
|
|
@ -404,8 +448,11 @@ class BaseDocument(object):
|
|||
self.db_insert()
|
||||
return
|
||||
|
||||
frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
|
||||
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
|
||||
if not ignore_if_duplicate:
|
||||
frappe.msgprint(_("{0} {1} already exists")
|
||||
.format(self.doctype, frappe.bold(self.name)),
|
||||
title=_("Duplicate Name"), indicator="red")
|
||||
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
|
||||
|
||||
elif frappe.db.is_unique_key_violation(e):
|
||||
# unique constraint
|
||||
|
|
@ -733,7 +780,7 @@ class BaseDocument(object):
|
|||
|
||||
type_map = frappe.db.type_map
|
||||
|
||||
for fieldname, value in self.get_valid_dict().items():
|
||||
for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
|
||||
df = self.meta.get_field(fieldname)
|
||||
|
||||
if not df or df.fieldtype == 'Check':
|
||||
|
|
@ -811,7 +858,7 @@ class BaseDocument(object):
|
|||
if frappe.flags.in_install:
|
||||
return
|
||||
|
||||
for fieldname, value in self.get_valid_dict().items():
|
||||
for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
|
||||
if not value or not isinstance(value, str):
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -249,11 +249,7 @@ class Document(BaseDocument):
|
|||
if getattr(self.meta, "issingle", 0):
|
||||
self.update_single(self.get_valid_dict())
|
||||
else:
|
||||
try:
|
||||
self.db_insert()
|
||||
except frappe.DuplicateEntryError as e:
|
||||
if not ignore_if_duplicate:
|
||||
raise e
|
||||
self.db_insert(ignore_if_duplicate=ignore_if_duplicate)
|
||||
|
||||
# children
|
||||
for d in self.get_all_children():
|
||||
|
|
|
|||
|
|
@ -14,16 +14,28 @@ Example:
|
|||
|
||||
|
||||
'''
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import click
|
||||
import frappe, json, os
|
||||
from frappe.utils import cstr, cint, cast
|
||||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields, child_table_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.base_document import BaseDocument
|
||||
from frappe.modules import load_doctype_module
|
||||
from frappe.model.workflow import get_workflow_name
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import (
|
||||
child_table_fields,
|
||||
data_fieldtypes,
|
||||
default_fields,
|
||||
no_value_fields,
|
||||
optional_fields,
|
||||
table_fields,
|
||||
)
|
||||
from frappe.model.base_document import BaseDocument
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.workflow import get_workflow_name
|
||||
from frappe.modules import load_doctype_module
|
||||
from frappe.utils import cast, cint, cstr
|
||||
|
||||
|
||||
def get_meta(doctype, cached=True):
|
||||
if cached:
|
||||
|
|
@ -444,9 +456,16 @@ class Meta(Document):
|
|||
self.permissions = [Document(d) for d in custom_perms]
|
||||
|
||||
def get_fieldnames_with_value(self, with_field_meta=False):
|
||||
return [df if with_field_meta else df.fieldname \
|
||||
for df in self.fields if df.fieldtype not in no_value_fields]
|
||||
def is_value_field(docfield):
|
||||
return not (
|
||||
docfield.get("is_virtual")
|
||||
or docfield.fieldtype in no_value_fields
|
||||
)
|
||||
|
||||
if with_field_meta:
|
||||
return [df for df in self.fields if is_value_field(df)]
|
||||
|
||||
return [df.fieldname for df in self.fields if is_value_field(df)]
|
||||
|
||||
def get_fields_to_check_permissions(self, user_permission_doctypes):
|
||||
fields = self.get("fields", {
|
||||
|
|
@ -546,7 +565,7 @@ class Meta(Document):
|
|||
# For internal links parent doctype will be the key
|
||||
doctype = link.parent_doctype or link.link_doctype
|
||||
# group found
|
||||
if link.group and group.label == link.group:
|
||||
if link.group and _(group.label) == _(link.group):
|
||||
if doctype not in group.get('items'):
|
||||
group.get('items').append(doctype)
|
||||
link.added = True
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ frappe.patches.v13_0.queryreport_columns
|
|||
frappe.patches.v13_0.jinja_hook
|
||||
frappe.patches.v13_0.update_notification_channel_if_empty
|
||||
frappe.patches.v13_0.set_first_day_of_the_week
|
||||
execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
|
||||
frappe.patches.v14_0.update_workspace2 # 20.09.2021
|
||||
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
|
||||
frappe.patches.v14_0.transform_todo_schema
|
||||
|
|
|
|||
|
|
@ -814,6 +814,13 @@
|
|||
<path d="M16.814 13.3304L17.9274 12.6875" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-dashboard-list">
|
||||
<path d="M7.5 2.5H4.5C3.94772 2.5 3.5 2.94772 3.5 3.5V9.5C3.5 10.0523 3.94772 10.5 4.5 10.5H7.5C8.05228 10.5 8.5 10.0523 8.5 9.5V3.5C8.5 2.94772 8.05228 2.5 7.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 13.5H4.5C3.94772 13.5 3.5 13.9477 3.5 14.5V16.5C3.5 17.0523 3.94772 17.5 4.5 17.5H7.5C8.05228 17.5 8.5 17.0523 8.5 16.5V14.5C8.5 13.9477 8.05228 13.5 7.5 13.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.5 2.5H12.5C11.9477 2.5 11.5 2.94772 11.5 3.5V6.5C11.5 7.05228 11.9477 7.5 12.5 7.5H15.5C16.0523 7.5 16.5 7.05228 16.5 6.5V3.5C16.5 2.94772 16.0523 2.5 15.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.5 10.5H12.5C11.9477 10.5 11.5 10.9477 11.5 11.5V16.5C11.5 17.0523 11.9477 17.5 12.5 17.5H15.5C16.0523 17.5 16.5 17.0523 16.5 16.5V11.5C16.5 10.9477 16.0523 10.5 15.5 10.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-text">
|
||||
<path d="M5 4V6.4H9V16H11.4V6.4H15.4V4H5Z" fill="var(--icon-stroke)" stroke="none"/>
|
||||
</symbol>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
|
|
@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl {
|
|||
if (this.df.get_status) {
|
||||
return this.df.get_status(this);
|
||||
}
|
||||
if (this.df.is_virtual) {
|
||||
return "Read";
|
||||
}
|
||||
|
||||
if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
|
||||
// like in case of a dialog box
|
||||
|
|
@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl {
|
|||
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
|
||||
return "None";
|
||||
|
||||
} else if (cint(this.df.read_only)) {
|
||||
} else if (cint(this.df.read_only || this.df.is_virtual)) {
|
||||
// eslint-disable-next-line
|
||||
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
|
||||
return "Read";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
setTimeout(function() {
|
||||
if(me.$input.val() && me.get_options()) {
|
||||
let doctype = me.get_options();
|
||||
let name = me.$input.val();
|
||||
let name = me.get_input_value();
|
||||
me.$link.toggle(true);
|
||||
me.$link_open.attr('href', frappe.utils.get_form_link(doctype, name));
|
||||
}
|
||||
|
|
@ -69,6 +69,59 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
this.$input_area.find(".link-btn").remove();
|
||||
}
|
||||
}
|
||||
set_formatted_input(value) {
|
||||
super.set_formatted_input();
|
||||
if (!value) return;
|
||||
|
||||
if (!this.title_value_map) {
|
||||
this.title_value_map = {};
|
||||
}
|
||||
this.set_link_title(value);
|
||||
}
|
||||
set_link_title(value) {
|
||||
let doctype = this.get_options();
|
||||
|
||||
if (!doctype) return;
|
||||
|
||||
if (in_list(frappe.boot.link_title_doctypes, doctype)) {
|
||||
let link_title = frappe.utils.get_link_title(doctype, value);
|
||||
if (!link_title) {
|
||||
link_title = frappe.utils
|
||||
.fetch_link_title(doctype, value)
|
||||
.then(link_title => {
|
||||
this.set_input_value(link_title);
|
||||
this.title_value_map[link_title] = value;
|
||||
});
|
||||
} else {
|
||||
this.set_input_value(link_title);
|
||||
this.title_value_map[link_title] = value;
|
||||
}
|
||||
} else {
|
||||
this.set_input_value(value);
|
||||
}
|
||||
}
|
||||
parse_validate_and_set_in_model(value, e, label) {
|
||||
if (this.parse) value = this.parse(value, label);
|
||||
if (label) {
|
||||
this.label = label;
|
||||
frappe.utils.add_link_title(this.df.options, value, label);
|
||||
}
|
||||
|
||||
return this.validate_and_set_in_model(value, e);
|
||||
}
|
||||
get_input_value() {
|
||||
if (this.$input) {
|
||||
const input_value = this.$input.val();
|
||||
return this.title_value_map?.[input_value] || input_value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
get_label_value() {
|
||||
return this.$input ? this.$input.val() : "";
|
||||
}
|
||||
set_input_value(value) {
|
||||
this.$input && this.$input.val(value);
|
||||
}
|
||||
open_advanced_search() {
|
||||
var doctype = this.get_options();
|
||||
if(!doctype) return;
|
||||
|
|
@ -98,7 +151,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
}
|
||||
|
||||
// partially entered name field
|
||||
frappe.route_options.name_field = this.get_value();
|
||||
frappe.route_options.name_field = this.get_label_value();
|
||||
|
||||
// reference to calling link
|
||||
frappe._from_link = this;
|
||||
|
|
@ -120,6 +173,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
maxItems: 99,
|
||||
autoFirst: true,
|
||||
list: [],
|
||||
replace: function (suggestion) {
|
||||
// Override Awesomeplete replace function as it is used to set the input value
|
||||
// https://github.com/LeaVerou/awesomplete/issues/17104#issuecomment-359185403
|
||||
this.input.value = suggestion.label || suggestion.value;
|
||||
},
|
||||
data: function (item) {
|
||||
return {
|
||||
label: item.label || item.value,
|
||||
|
|
@ -236,9 +294,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
me.selected = false;
|
||||
return;
|
||||
}
|
||||
var value = me.get_input_value();
|
||||
if(value!==me.last_value) {
|
||||
me.parse_validate_and_set_in_model(value);
|
||||
let value = me.get_input_value();
|
||||
let label = me.get_label_value();
|
||||
|
||||
if (value !== me.last_value || me.label !== label) {
|
||||
me.parse_validate_and_set_in_model(value, null, label);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -258,14 +318,15 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
|
||||
// prevent selection on tab
|
||||
var TABKEY = 9;
|
||||
if(e.keyCode === TABKEY) {
|
||||
if (e.keyCode === TABKEY) {
|
||||
e.preventDefault();
|
||||
me.awesomplete.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(item.action) {
|
||||
if (item.action) {
|
||||
item.value = "";
|
||||
item.label = "";
|
||||
item.action.apply(me);
|
||||
}
|
||||
|
||||
|
|
@ -277,12 +338,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
frappe.boot.user.last_selected_values[me.df.options] = item.value;
|
||||
}
|
||||
|
||||
me.parse_validate_and_set_in_model(item.value);
|
||||
me.parse_validate_and_set_in_model(item.value, null, item.label);
|
||||
});
|
||||
|
||||
this.$input.on("awesomplete-selectcomplete", function(e) {
|
||||
var o = e.originalEvent;
|
||||
if(o.text.value.indexOf("__link_option") !== -1) {
|
||||
let o = e.originalEvent;
|
||||
if (o.text.value.indexOf("__link_option") !== -1) {
|
||||
me.$input.val("");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,15 +83,21 @@ frappe.ui.form.ControlMultiSelectPills = class ControlMultiSelectPills extends f
|
|||
}
|
||||
|
||||
get_pill_html(value) {
|
||||
const label = this.get_label(value);
|
||||
const encoded_value = encodeURIComponent(value);
|
||||
return `
|
||||
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
|
||||
<span class="btn-link-to-form">${__(value)}</span>
|
||||
<span class="btn-link-to-form">${__(label || value)}</span>
|
||||
<span class="btn-remove">${frappe.utils.icon('close')}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
get_label(value) {
|
||||
const item = this._data?.find(d => d.value === value);
|
||||
return item ? item.label || item.value : null;
|
||||
}
|
||||
|
||||
get_awesomplete_settings() {
|
||||
const settings = super.get_awesomplete_settings();
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
|
|||
setup_buttons() {
|
||||
this.$input_area.find('.link-btn').remove();
|
||||
}
|
||||
parse(value) {
|
||||
parse(value, label) {
|
||||
const link_field = this.get_link_field();
|
||||
|
||||
if (value) {
|
||||
|
|
@ -62,6 +62,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
|
|||
[link_field.fieldname]: value
|
||||
});
|
||||
}
|
||||
frappe.utils.add_link_title(link_field.options, value, label);
|
||||
}
|
||||
this._rows_list = this.rows.map(row => row[link_field.fieldname]);
|
||||
return this.rows;
|
||||
|
|
@ -126,10 +127,12 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
|
|||
this.$input_area.prepend(html);
|
||||
}
|
||||
get_pill_html(value) {
|
||||
const link_field = this.get_link_field();
|
||||
const encoded_value = encodeURIComponent(value);
|
||||
const pill_name = frappe.utils.get_link_title(link_field.options, value) || value;
|
||||
return `
|
||||
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
|
||||
<span class="btn-link-to-form">${__(value)}</span>
|
||||
<span class="btn-link-to-form">${__(pill_name)}</span>
|
||||
<span class="btn-remove">${frappe.utils.icon('close')}</span>
|
||||
</button>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.doc = frappe.get_doc(this.doctype, this.docname);
|
||||
|
||||
// check permissions
|
||||
if(!this.has_read_permission()) {
|
||||
if (!this.has_read_permission()) {
|
||||
frappe.show_not_permitted(__(this.doctype) + " " + __(this.docname));
|
||||
return;
|
||||
}
|
||||
|
|
@ -1363,6 +1363,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
set_df_property(fieldname, property, value, docname, table_field, table_row_name=null) {
|
||||
let df;
|
||||
|
||||
if (!docname || !table_field) {
|
||||
df = this.get_docfield(fieldname);
|
||||
} else {
|
||||
|
|
@ -1372,8 +1373,10 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
df = frappe.meta.get_docfield(filtered_fields[0].parent, table_field, table_row_name);
|
||||
}
|
||||
}
|
||||
|
||||
if (df && df[property] != value) {
|
||||
df[property] = value;
|
||||
|
||||
if (table_field && table_row_name) {
|
||||
if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) {
|
||||
this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname);
|
||||
|
|
@ -1661,23 +1664,17 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
// make new doctype from the current form
|
||||
// will handover to `make_methods` if defined
|
||||
// or will create and match link fields
|
||||
var me = this;
|
||||
let me = this;
|
||||
if(this.make_methods && this.make_methods[doctype]) {
|
||||
return this.make_methods[doctype](this);
|
||||
} else if(this.custom_make_buttons && this.custom_make_buttons[doctype]) {
|
||||
this.custom_buttons[__(this.custom_make_buttons[doctype])].trigger('click');
|
||||
} else {
|
||||
frappe.model.with_doctype(doctype, function() {
|
||||
var new_doc = frappe.model.get_new_doc(doctype);
|
||||
let new_doc = frappe.model.get_new_doc(doctype, null, null, true);
|
||||
|
||||
// set link fields (if found)
|
||||
frappe.get_meta(doctype).fields.forEach(function(df) {
|
||||
if(df.fieldtype==='Link' && df.options===me.doctype) {
|
||||
new_doc[df.fieldname] = me.doc.name;
|
||||
} else if (['Link', 'Dynamic Link'].includes(df.fieldtype) && me.doc[df.fieldname]) {
|
||||
new_doc[df.fieldname] = me.doc[df.fieldname];
|
||||
}
|
||||
});
|
||||
me.set_link_field(doctype, new_doc);
|
||||
|
||||
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
|
||||
// frappe.set_route('Form', doctype, new_doc.name);
|
||||
|
|
@ -1685,6 +1682,20 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
}
|
||||
|
||||
set_link_field(doctype, new_doc) {
|
||||
let me = this;
|
||||
frappe.get_meta(doctype).fields.forEach(function(df) {
|
||||
if (df.fieldtype === 'Link' && df.options === me.doctype) {
|
||||
new_doc[df.fieldname] = me.doc.name;
|
||||
} else if (['Link', 'Dynamic Link'].includes(df.fieldtype) && me.doc[df.fieldname]) {
|
||||
new_doc[df.fieldname] = me.doc[df.fieldname];
|
||||
} else if (df.fieldtype === 'Table' && df.options && df.reqd) {
|
||||
let row = new_doc[df.fieldname][0];
|
||||
me.set_link_field(df.options, row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update_in_all_rows(table_fieldname, fieldname, value) {
|
||||
// update the child value in all tables where it is missing
|
||||
if(!value) return;
|
||||
|
|
|
|||
|
|
@ -110,12 +110,14 @@ frappe.form.formatters = {
|
|||
Link: function(value, docfield, options, doc) {
|
||||
var doctype = docfield._options || docfield.options;
|
||||
var original_value = value;
|
||||
let link_title = frappe.utils.get_link_title(doctype, value);
|
||||
|
||||
if(value && value.match && value.match(/^['"].*['"]$/)) {
|
||||
value.replace(/^.(.*).$/, "$1");
|
||||
}
|
||||
|
||||
if(options && (options.for_print || options.only_value)) {
|
||||
return value;
|
||||
return link_title || value;
|
||||
}
|
||||
|
||||
if(frappe.form.link_formatters[doctype]) {
|
||||
|
|
@ -139,13 +141,14 @@ frappe.form.formatters = {
|
|||
return `<a
|
||||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
|
||||
data-doctype="${doctype}"
|
||||
data-name="${original_value}">
|
||||
${__(options && options.label || value)}</a>`;
|
||||
data-name="${original_value}"
|
||||
data-value="${original_value}">
|
||||
${__(options && options.label || link_title || value)}</a>`;
|
||||
} else {
|
||||
return value;
|
||||
return link_title || value;
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
return link_title || value;
|
||||
}
|
||||
},
|
||||
Date: function(value) {
|
||||
|
|
|
|||
|
|
@ -502,10 +502,9 @@ export default class Grid {
|
|||
|
||||
set_column_disp(fieldname, show) {
|
||||
if ($.isArray(fieldname)) {
|
||||
for (var i = 0, l = fieldname.length; i < l; i++) {
|
||||
var fname = fieldname[i];
|
||||
this.get_docfield(fname).hidden = show ? 0 : 1;
|
||||
this.set_editable_grid_column_disp(fname, show);
|
||||
for (let field of fieldname) {
|
||||
this.update_docfield_property(field, "hidden", show);
|
||||
this.set_editable_grid_column_disp(field, show);
|
||||
}
|
||||
} else {
|
||||
this.get_docfield(fieldname).hidden = show ? 0 : 1;
|
||||
|
|
@ -555,17 +554,17 @@ export default class Grid {
|
|||
}
|
||||
|
||||
toggle_reqd(fieldname, reqd) {
|
||||
this.get_docfield(fieldname).reqd = reqd;
|
||||
this.update_docfield_property(fieldname, "reqd", reqd);
|
||||
this.debounced_refresh();
|
||||
}
|
||||
|
||||
toggle_enable(fieldname, enable) {
|
||||
this.get_docfield(fieldname).read_only = enable ? 0 : 1;
|
||||
this.update_docfield_property(fieldname, "read_only", enable ? 0 : 1);
|
||||
this.debounced_refresh();
|
||||
}
|
||||
|
||||
toggle_display(fieldname, show) {
|
||||
this.get_docfield(fieldname).hidden = show ? 0 : 1;
|
||||
this.update_docfield_property(fieldname, "hidden", show ? 0 : 1);
|
||||
this.debounced_refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ export default class GridRow {
|
|||
this.on_grid_fields_dict = {};
|
||||
this.on_grid_fields = [];
|
||||
$.extend(this, opts);
|
||||
if (this.doc && this.parent_df.options) {
|
||||
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
|
||||
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
this.docfields = docfields.length ? docfields : opts.docfields;
|
||||
}
|
||||
this.set_docfields();
|
||||
this.columns = {};
|
||||
this.columns_list = [];
|
||||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
|
||||
|
|
@ -41,6 +37,22 @@ export default class GridRow {
|
|||
this.set_data();
|
||||
}
|
||||
}
|
||||
|
||||
set_docfields(update=false) {
|
||||
if (this.doc && this.parent_df.options) {
|
||||
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
|
||||
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
if (update) {
|
||||
// to maintain references
|
||||
this.docfields.forEach(df => {
|
||||
Object.assign(df, docfields.find(d => d.fieldname === df.fieldname));
|
||||
});
|
||||
} else {
|
||||
this.docfields = docfields;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_data() {
|
||||
this.wrapper.data({
|
||||
"doc": this.doc
|
||||
|
|
@ -148,6 +160,11 @@ export default class GridRow {
|
|||
}, __('Move To'), 'Update');
|
||||
}
|
||||
refresh() {
|
||||
// update docfields for new record
|
||||
if (this.frm && this.doc && this.doc.__islocal) {
|
||||
this.set_docfields(true);
|
||||
}
|
||||
|
||||
if(this.frm && this.doc) {
|
||||
this.doc = locals[this.doc.doctype][this.doc.name];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -547,24 +547,28 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
|
||||
refresh_dependency() {
|
||||
// Resolve "depends_on" and show / hide accordingly
|
||||
/**
|
||||
Resolve "depends_on" and show / hide accordingly
|
||||
build dependants' dictionary
|
||||
*/
|
||||
|
||||
// build dependants' dictionary
|
||||
let has_dep = false;
|
||||
|
||||
for (let fkey in this.fields_list) {
|
||||
let f = this.fields_list[fkey];
|
||||
f.dependencies_clear = true;
|
||||
const fields = this.fields_list.concat(this.tabs);
|
||||
|
||||
for (let fkey in fields) {
|
||||
let f = fields[fkey];
|
||||
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
|
||||
has_dep = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_dep) return;
|
||||
|
||||
// show / hide based on values
|
||||
for (let i = this.fields_list.length - 1; i >= 0; i--) {
|
||||
let f = this.fields_list[i];
|
||||
for (let i = fields.length - 1; i >= 0; i--) {
|
||||
let f = fields[i];
|
||||
f.guardian_has_value = true;
|
||||
if (f.df.depends_on) {
|
||||
// evaluate guardian
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
||||
constructor(opts) {
|
||||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
|
||||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label, columns */
|
||||
Object.assign(this, opts);
|
||||
this.for_select = this.doctype == "[Select]";
|
||||
if (!this.for_select) {
|
||||
|
|
@ -400,23 +400,22 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
return this.results.filter(res => checked_values.includes(res.name));
|
||||
}
|
||||
|
||||
get_datatable_columns() {
|
||||
if (this.get_query && this.get_query().query && this.columns) return this.columns;
|
||||
|
||||
if (Array.isArray(this.setters))
|
||||
return ["name", ...this.setters.map(df => df.fieldname)];
|
||||
|
||||
return ["name", ...Object.keys(this.setters)];
|
||||
}
|
||||
|
||||
make_list_row(result = {}) {
|
||||
var me = this;
|
||||
// Make a head row by default (if result not passed)
|
||||
let head = Object.keys(result).length === 0;
|
||||
|
||||
let contents = ``;
|
||||
let columns = ["name"];
|
||||
|
||||
if ($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
columns.push(df.fieldname);
|
||||
}
|
||||
} else {
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
}
|
||||
|
||||
columns.forEach(function (column) {
|
||||
this.get_datatable_columns().forEach(function (column) {
|
||||
contents += `<div class="list-item__content ellipsis">
|
||||
${
|
||||
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
|
||||
|
|
@ -486,7 +485,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
|
||||
get_filters_from_setters() {
|
||||
let me = this;
|
||||
let filters = this.get_query ? this.get_query().filters : {} || {};
|
||||
let filters = (this.get_query ? this.get_query().filters : {}) || {};
|
||||
let filter_fields = [];
|
||||
|
||||
if ($.isArray(this.setters)) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
|
|||
|
||||
// prepare a list of mandatory, bold and allow in quick entry fields
|
||||
this.mandatory = fields.filter(df => {
|
||||
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
|
||||
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -249,30 +249,39 @@ frappe.ui.form.update_calling_link = (newdoc) => {
|
|||
};
|
||||
|
||||
if (is_valid_doctype()) {
|
||||
// set value
|
||||
if (doc && doc.parentfield) {
|
||||
//update values for child table
|
||||
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
|
||||
if (field.doc && field.doc.name === frappe._from_link.docname) {
|
||||
frappe._from_link.set_value(newdoc.name);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frappe._from_link.set_value(newdoc.name);
|
||||
}
|
||||
|
||||
// refresh field
|
||||
frappe._from_link.refresh();
|
||||
|
||||
// if from form, switch
|
||||
if (frappe._from_link.frm) {
|
||||
frappe.set_route("Form",
|
||||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
|
||||
.then(() => {
|
||||
frappe.utils.scroll_to(frappe._from_link_scrollY);
|
||||
frappe.model.with_doctype(newdoc.doctype, () => {
|
||||
let meta = frappe.get_meta(newdoc.doctype);
|
||||
// set value
|
||||
if (doc && doc.parentfield) {
|
||||
//update values for child table
|
||||
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
|
||||
if (field.doc && field.doc.name === frappe._from_link.docname) {
|
||||
if (meta.title_field && meta.show_title_field_in_link) {
|
||||
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
|
||||
}
|
||||
frappe._from_link.set_value(newdoc.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (meta.title_field && meta.show_title_field_in_link) {
|
||||
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
|
||||
}
|
||||
frappe._from_link.set_value(newdoc.name);
|
||||
}
|
||||
|
||||
frappe._from_link = null;
|
||||
// refresh field
|
||||
frappe._from_link.refresh();
|
||||
|
||||
// if from form, switch
|
||||
if (frappe._from_link.frm) {
|
||||
frappe.set_route("Form",
|
||||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
|
||||
.then(() => {
|
||||
frappe.utils.scroll_to(frappe._from_link_scrollY);
|
||||
});
|
||||
}
|
||||
|
||||
frappe._from_link = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager {
|
|||
}
|
||||
|
||||
function setup_add_fetch(df) {
|
||||
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image',
|
||||
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
|
||||
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
|
||||
let is_read_only_field = (
|
||||
['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image',
|
||||
'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype)
|
||||
|| df.read_only == 1
|
||||
|| df.is_virtual == 1
|
||||
)
|
||||
|
||||
if (
|
||||
is_read_only_field
|
||||
&& df.fetch_from
|
||||
&& df.fetch_from.indexOf(".") != -1
|
||||
) {
|
||||
var parts = df.fetch_from.split(".");
|
||||
me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default class Tab {
|
|||
hide = true;
|
||||
}
|
||||
|
||||
hide && this.toggle(false);
|
||||
this.toggle(!hide);
|
||||
}
|
||||
|
||||
toggle(show) {
|
||||
|
|
|
|||
|
|
@ -391,10 +391,10 @@ frappe.views.BaseList = class BaseList {
|
|||
$this.addClass("btn-info");
|
||||
|
||||
this.start = 0;
|
||||
this.page_length = $this.data().value;
|
||||
this.page_length = this.selected_page_count = $this.data().value;
|
||||
} else if ($this.is(".btn-more")) {
|
||||
this.start = this.start + this.page_length;
|
||||
this.page_length = 20;
|
||||
this.page_length = this.selected_page_count || 20;
|
||||
}
|
||||
this.refresh();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1483,7 +1483,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
return [
|
||||
filter[1],
|
||||
"=",
|
||||
JSON.stringify([filter[2], filter[3]]),
|
||||
encodeURIComponent(JSON.stringify([filter[2], filter[3]])),
|
||||
].join("");
|
||||
})
|
||||
.join("&");
|
||||
|
|
@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
frappe.model.is_value_type(field_doc) &&
|
||||
field_doc.fieldtype !== "Read Only" &&
|
||||
!field_doc.hidden &&
|
||||
!field_doc.read_only
|
||||
!field_doc.read_only &&
|
||||
!field_doc.is_virtual
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,14 @@ frappe.request.call = function(opts) {
|
|||
$.extend(frappe._messages, data.__messages);
|
||||
}
|
||||
|
||||
// sync link titles
|
||||
if (data._link_titles) {
|
||||
if (!frappe._link_titles) {
|
||||
frappe._link_titles = {};
|
||||
}
|
||||
$.extend(frappe._link_titles, data._link_titles);
|
||||
}
|
||||
|
||||
// callbacks
|
||||
var status_code_handler = statusCode[xhr.statusCode().status];
|
||||
if (status_code_handler) {
|
||||
|
|
|
|||
|
|
@ -314,6 +314,10 @@ frappe.ui.Filter = class {
|
|||
return this.utils.get_selected_value(this.field, this.get_condition());
|
||||
}
|
||||
|
||||
get_selected_label() {
|
||||
return this.utils.get_selected_label(this.field);
|
||||
}
|
||||
|
||||
get_condition() {
|
||||
return this.filter_edit_area.find('.condition').val();
|
||||
}
|
||||
|
|
@ -361,7 +365,7 @@ frappe.ui.Filter = class {
|
|||
get_filter_button_text() {
|
||||
let value = this.utils.get_formatted_value(
|
||||
this.field,
|
||||
this.get_selected_value()
|
||||
this.get_selected_label() || this.get_selected_value()
|
||||
);
|
||||
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
|
||||
value
|
||||
|
|
@ -449,6 +453,12 @@ frappe.ui.filter_utils = {
|
|||
return val;
|
||||
},
|
||||
|
||||
get_selected_label(field) {
|
||||
if (in_list(["Link", "Dynamic Link"], field.df.fieldtype)) {
|
||||
return field.get_label_value();
|
||||
}
|
||||
},
|
||||
|
||||
get_default_condition(df) {
|
||||
if (df.fieldtype == 'Data') {
|
||||
return 'like';
|
||||
|
|
|
|||
|
|
@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) {
|
|||
'/': '/'
|
||||
};
|
||||
const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14
|
||||
const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt
|
||||
options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty.
|
||||
|
||||
// Rule 3 - TODO: Check event handlers?
|
||||
// script and alert should be checked first or else it will be escaped
|
||||
if (options.strategies.includes('js')) {
|
||||
sanitised = sanitised.replace(REGEX_SCRIPT, "");
|
||||
sanitised = sanitised.replace(REGEX_ALERT, "");
|
||||
}
|
||||
|
||||
// Rule 1
|
||||
if (options.strategies.includes('html')) {
|
||||
for (let char in HTML_ESCAPE_MAP) {
|
||||
|
|
@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) {
|
|||
}
|
||||
}
|
||||
|
||||
// Rule 3 - TODO: Check event handlers?
|
||||
if (options.strategies.includes('js')) {
|
||||
sanitised = sanitised.replace(REGEX_SCRIPT, "");
|
||||
}
|
||||
|
||||
return sanitised;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1416,5 +1416,42 @@ Object.assign(frappe.utils, {
|
|||
arr.push(i);
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
|
||||
get_link_title(doctype, name) {
|
||||
if (!doctype || !name || !frappe._link_titles) {
|
||||
return;
|
||||
}
|
||||
|
||||
return frappe._link_titles[doctype + "::" + name];
|
||||
},
|
||||
|
||||
add_link_title(doctype, name, value) {
|
||||
if (!doctype || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frappe._link_titles) {
|
||||
// for link titles
|
||||
frappe._link_titles = {};
|
||||
}
|
||||
|
||||
frappe._link_titles[doctype + "::" + name] = value;
|
||||
},
|
||||
|
||||
fetch_link_title(doctype, name) {
|
||||
try {
|
||||
return frappe.xcall("frappe.desk.search.get_link_title", {
|
||||
"doctype": doctype,
|
||||
"docname": name
|
||||
}).then(title => {
|
||||
frappe.utils.add_link_title(doctype, name, title);
|
||||
return title;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error while fetching link title.'); // eslint-disable-line
|
||||
console.log(error); // eslint-disable-line
|
||||
return Promise.resolve(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -578,6 +578,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
args: {
|
||||
report_name: this.report_name,
|
||||
filters: filters,
|
||||
report_settings: this.report_settings
|
||||
},
|
||||
callback: resolve,
|
||||
always: () => this.page.btn_secondary.prop('disabled', false)
|
||||
|
|
@ -834,7 +835,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
let data = this.data;
|
||||
let columns = this.columns.filter((col) => !col.hidden);
|
||||
|
||||
if (this.raw_data.add_total_row) {
|
||||
if (this.raw_data.add_total_row && !this.report_settings.tree) {
|
||||
data = data.slice();
|
||||
data.splice(-1, 1);
|
||||
}
|
||||
|
|
@ -854,7 +855,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
treeView: this.tree_report,
|
||||
layout: 'fixed',
|
||||
cellHeight: 33,
|
||||
showTotalRow: this.raw_data.add_total_row,
|
||||
showTotalRow: this.raw_data.add_total_row && !this.report_settings.tree,
|
||||
direction: frappe.utils.is_rtl() ? 'rtl' : 'ltr',
|
||||
hooks: {
|
||||
columnTotal: frappe.utils.report_column_total
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
setup_defaults() {
|
||||
super.setup_defaults();
|
||||
this.page_title = __('Report:') + ' ' + this.page_title;
|
||||
this.menu_items = this.report_menu_items();
|
||||
this.view = 'Report';
|
||||
|
||||
const route = frappe.get_route();
|
||||
|
|
@ -52,6 +51,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
this.page.main.addClass('report-view');
|
||||
}
|
||||
|
||||
setup_page() {
|
||||
this.menu_items = this.report_menu_items();
|
||||
super.setup_page();
|
||||
}
|
||||
|
||||
toggle_side_bar() {
|
||||
super.toggle_side_bar();
|
||||
// refresh datatable when sidebar is toggled to accomodate extra space
|
||||
|
|
@ -644,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
// not a cancelled doc
|
||||
&& data.docstatus !== 2
|
||||
&& !df.read_only
|
||||
&& !df.is_virtual
|
||||
&& !df.hidden
|
||||
// not a standard field i.e., owner, modified_by, etc.
|
||||
&& frappe.model.is_non_std_field(df.fieldname))
|
||||
|
|
@ -1025,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
title += ` (${__(doctype)})`;
|
||||
}
|
||||
|
||||
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only;
|
||||
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual;
|
||||
|
||||
const align = (() => {
|
||||
const is_numeric = frappe.model.is_numeric_field(docfield);
|
||||
|
|
@ -1207,7 +1212,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
args: {
|
||||
name: name,
|
||||
doctype: this.doctype,
|
||||
json: JSON.stringify(report_settings)
|
||||
report_settings: JSON.stringify(report_settings)
|
||||
},
|
||||
callback:(r) => {
|
||||
if(r.exc) {
|
||||
|
|
@ -1244,6 +1249,17 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
}
|
||||
|
||||
delete_report() {
|
||||
return frappe.call({
|
||||
method: 'frappe.desk.reportview.delete_report',
|
||||
args: { name: this.report_name },
|
||||
callback(response) {
|
||||
if (response.exc) return;
|
||||
window.history.back();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_column_widths() {
|
||||
if (this.datatable) {
|
||||
return this.datatable
|
||||
|
|
@ -1465,12 +1481,42 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
});
|
||||
|
||||
// save buttons
|
||||
if(frappe.user.is_report_manager()) {
|
||||
items = items.concat([
|
||||
{ label: __('Save'), action: () => this.save_report('save') },
|
||||
{ label: __('Save As'), action: () => this.save_report('save_as') }
|
||||
]);
|
||||
const can_edit_or_delete = (action) => {
|
||||
const method = action == "delete" ? "can_delete" : "can_write";
|
||||
return (
|
||||
this.report_doc
|
||||
&& this.report_doc.is_standard !== "Yes"
|
||||
&& (
|
||||
frappe.model[method]("Report")
|
||||
|| this.report_doc.owner === frappe.session.user
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// A user with role Report Manager or Report Owner can save
|
||||
if (can_edit_or_delete()) {
|
||||
items.push({
|
||||
label: __("Save"),
|
||||
action: () => this.save_report('save')
|
||||
});
|
||||
}
|
||||
|
||||
// anyone can save as
|
||||
items.push({
|
||||
label: __('Save As'),
|
||||
action: () => this.save_report('save_as')
|
||||
});
|
||||
|
||||
// A user with role Report Manager or Report Owner can delete
|
||||
if (can_edit_or_delete("delete")) {
|
||||
items.push({
|
||||
label: __("Delete"),
|
||||
action: () => frappe.confirm(
|
||||
"Are you sure you want to delete this report?",
|
||||
() => this.delete_report(),
|
||||
),
|
||||
shortcut: "Shift+Ctrl+D"
|
||||
});
|
||||
}
|
||||
|
||||
// user permissions
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView {
|
|||
this.ignore_fields = this.opts.ignore_fields || [];
|
||||
|
||||
var mandatory_fields = $.map(me.opts.meta.fields, function(d) {
|
||||
return (d.reqd || d.bold && !d.read_only) ? d : null });
|
||||
return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null });
|
||||
|
||||
var opts_field_names = this.fields.map(function(d) {
|
||||
return d.fieldname
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@
|
|||
--shadow-md: 0px 8px 14px rgba(25, 39, 52, 0.08), 0px 2px 6px rgba(25, 39, 52, 0.04);
|
||||
--shadow-lg: 0px 18px 22px rgba(25, 39, 52, 0.1), 0px 1px 10px rgba(0, 0, 0, 0.06), 0px 0.5px 5px rgba(25, 39, 52, 0.04);
|
||||
|
||||
--drop-shadow: 0px 0.5px 0px rgba(0, 0, 0, 0.05), 0px 0px 0px rgba(0, 0, 0, 0), 0px 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--modal-shadow: var(--shadow-md);
|
||||
--card-shadow: var(--shadow-sm);
|
||||
--btn-shadow: var(--shadow-xs);
|
||||
|
|
|
|||
|
|
@ -187,7 +187,31 @@ $level-margin-right: 8px;
|
|||
}
|
||||
|
||||
.list-paging-area, .footnote-area {
|
||||
border-top: 1px sol var(--border-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
|
||||
.btn-group {
|
||||
box-shadow: var(--drop-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
&> .btn:nth-child(2) {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.btn-paging {
|
||||
box-shadow: none;
|
||||
margin-left: 0px !important;
|
||||
border: 1px solid var(--dark-border-color);
|
||||
|
||||
&.btn-info {
|
||||
background-color: var(--gray-400);
|
||||
border-color: var(--gray-400);
|
||||
color: var(--white);
|
||||
font-weight: var(--text-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.frappe-card {
|
||||
|
|
|
|||
|
|
@ -104,10 +104,10 @@ body[data-route^="Module"] .main-menu {
|
|||
}
|
||||
|
||||
.sidebar-image-section {
|
||||
width: min(100%, 170px);
|
||||
cursor: pointer;
|
||||
|
||||
.sidebar-image {
|
||||
width: min(100%, 170px);
|
||||
height: auto;
|
||||
max-height: 170px;
|
||||
object-fit: cover;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{% for page in layout %}
|
||||
<div class="page-break">
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
|
||||
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, print_heading_template) }}
|
||||
</div>
|
||||
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
|
|
|
|||
|
|
@ -186,12 +186,12 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
|
|||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None) -%}
|
||||
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
{% endif %}
|
||||
{% if doc.print_heading_template %}
|
||||
{{ frappe.render_template(doc.print_heading_template, {"doc":doc}) }}
|
||||
{% if print_heading_template %}
|
||||
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
|
||||
{% else %}
|
||||
<div class="print-heading">
|
||||
<h2>
|
||||
|
|
|
|||
|
|
@ -1,77 +1,126 @@
|
|||
import sys
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from random import choice
|
||||
from threading import Thread
|
||||
from typing import Dict, Optional, Tuple
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
from semantic_version import Version
|
||||
from werkzeug.test import TestResponse
|
||||
|
||||
import frappe
|
||||
from frappe.utils import get_site_url
|
||||
from frappe.utils import get_site_url, get_test_client
|
||||
|
||||
try:
|
||||
_site = frappe.local.site
|
||||
except Exception:
|
||||
_site = None
|
||||
|
||||
authorization_token = None
|
||||
|
||||
@contextmanager
|
||||
def suppress_stdout():
|
||||
"""Supress stdout for tests which expectedly make noise
|
||||
but that you don't need in tests"""
|
||||
sys.stdout = None
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = sys.__stdout__
|
||||
|
||||
|
||||
def maintain_state(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
frappe.db.rollback()
|
||||
r = f(*args, **kwargs)
|
||||
frappe.db.commit()
|
||||
return r
|
||||
|
||||
return wrapper
|
||||
def make_request(target: str, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None) -> TestResponse:
|
||||
t = ThreadWithReturnValue(target=target, args=args, kwargs=kwargs)
|
||||
t.start()
|
||||
t.join()
|
||||
return t._return
|
||||
|
||||
|
||||
class TestResourceAPI(unittest.TestCase):
|
||||
SITE_URL = get_site_url(frappe.local.site)
|
||||
def patch_request_header(key, *args, **kwargs):
|
||||
if key == "Authorization":
|
||||
return f"token {authorization_token}"
|
||||
|
||||
|
||||
class ThreadWithReturnValue(Thread):
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
Thread.__init__(self, group, target, name, args, kwargs)
|
||||
self._return = None
|
||||
|
||||
def run(self):
|
||||
if self._target is not None:
|
||||
with patch("frappe.app.get_site_name", return_value=_site):
|
||||
header_patch = patch("frappe.get_request_header", new=patch_request_header)
|
||||
if authorization_token:
|
||||
header_patch.start()
|
||||
self._return = self._target(*self._args, **self._kwargs)
|
||||
if authorization_token:
|
||||
header_patch.stop()
|
||||
|
||||
def join(self, *args):
|
||||
Thread.join(self, *args)
|
||||
return self._return
|
||||
|
||||
|
||||
class FrappeAPITestCase(unittest.TestCase):
|
||||
SITE = frappe.local.site
|
||||
SITE_URL = get_site_url(SITE)
|
||||
RESOURCE_URL = f"{SITE_URL}/api/resource"
|
||||
TEST_CLIENT = get_test_client()
|
||||
|
||||
@property
|
||||
def sid(self) -> str:
|
||||
if not getattr(self, "_sid", None):
|
||||
r = self.post("/api/method/login", {
|
||||
"usr": "Administrator",
|
||||
"pwd": frappe.conf.admin_password or "admin",
|
||||
})
|
||||
self._sid = r.headers[2][1].split(";")[0].lstrip("sid=")
|
||||
|
||||
return self._sid
|
||||
|
||||
def get(self, path: str, params: Optional[Dict] = None) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.get, args=(path, ), kwargs={"data": params})
|
||||
|
||||
def post(self, path, data) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.post, args=(path, ), kwargs={"data": data})
|
||||
|
||||
def put(self, path, data) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.put, args=(path, ), kwargs={"data": data})
|
||||
|
||||
def delete(self, path) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.delete, args=(path, ))
|
||||
|
||||
|
||||
class TestResourceAPI(FrappeAPITestCase):
|
||||
DOCTYPE = "ToDo"
|
||||
GENERATED_DOCUMENTS = []
|
||||
|
||||
@classmethod
|
||||
@maintain_state
|
||||
def setUpClass(self):
|
||||
def setUpClass(cls):
|
||||
for _ in range(10):
|
||||
doc = frappe.get_doc(
|
||||
{"doctype": "ToDo", "description": frappe.mock("paragraph")}
|
||||
).insert()
|
||||
self.GENERATED_DOCUMENTS.append(doc.name)
|
||||
cls.GENERATED_DOCUMENTS.append(doc.name)
|
||||
frappe.db.commit()
|
||||
|
||||
@classmethod
|
||||
@maintain_state
|
||||
def tearDownClass(self):
|
||||
for name in self.GENERATED_DOCUMENTS:
|
||||
frappe.delete_doc_if_exists(self.DOCTYPE, name)
|
||||
def tearDownClass(cls):
|
||||
for name in cls.GENERATED_DOCUMENTS:
|
||||
frappe.delete_doc_if_exists(cls.DOCTYPE, name)
|
||||
frappe.db.commit()
|
||||
|
||||
def setUp(self):
|
||||
# commit to ensure consistency in session (postgres CI randomly fails)
|
||||
if frappe.conf.db_type == "postgres":
|
||||
frappe.db.commit()
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
if not getattr(self, "_sid", None):
|
||||
self._sid = requests.post(
|
||||
f"{self.SITE_URL}/api/method/login",
|
||||
data={
|
||||
"usr": "Administrator",
|
||||
"pwd": frappe.conf.admin_password or "admin",
|
||||
},
|
||||
).cookies.get("sid")
|
||||
|
||||
return self._sid
|
||||
|
||||
def get(self, path, params=""):
|
||||
return requests.get(f"{self.RESOURCE_URL}/{path}?sid={self.sid}{params}")
|
||||
|
||||
def post(self, path, data):
|
||||
return requests.post(
|
||||
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
|
||||
)
|
||||
|
||||
def put(self, path, data):
|
||||
return requests.put(
|
||||
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
|
||||
)
|
||||
|
||||
def delete(self, path):
|
||||
return requests.delete(f"{self.RESOURCE_URL}/{path}?sid={self.sid}")
|
||||
if self._testMethodName == "test_auth_cycle":
|
||||
from frappe.core.doctype.user.user import generate_keys
|
||||
generate_keys("Administrator")
|
||||
frappe.db.commit()
|
||||
|
||||
def test_unauthorized_call(self):
|
||||
# test 1: fetch documents without auth
|
||||
|
|
@ -80,88 +129,107 @@ class TestResourceAPI(unittest.TestCase):
|
|||
|
||||
def test_get_list(self):
|
||||
# test 2: fetch documents without params
|
||||
response = self.get(self.DOCTYPE)
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json(), dict)
|
||||
self.assertIn("data", response.json())
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn("data", response.json)
|
||||
|
||||
def test_get_list_limit(self):
|
||||
# test 3: fetch data with limit
|
||||
response = self.get(self.DOCTYPE, "&limit=2")
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "limit": 2})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.json()["data"]), 2)
|
||||
self.assertEqual(len(response.json["data"]), 2)
|
||||
|
||||
def test_get_list_dict(self):
|
||||
# test 4: fetch response as (not) dict
|
||||
response = self.get(self.DOCTYPE, "&as_dict=True")
|
||||
json = frappe._dict(response.json())
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": True})
|
||||
json = frappe._dict(response.json)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(json.data, list)
|
||||
self.assertIsInstance(json.data[0], dict)
|
||||
|
||||
response = self.get(self.DOCTYPE, "&as_dict=False")
|
||||
json = frappe._dict(response.json())
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": False})
|
||||
json = frappe._dict(response.json)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(json.data, list)
|
||||
self.assertIsInstance(json.data[0], list)
|
||||
|
||||
def test_get_list_debug(self):
|
||||
# test 5: fetch response with debug
|
||||
response = self.get(self.DOCTYPE, "&debug=true")
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "debug": True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("exc", response.json())
|
||||
self.assertIsInstance(response.json()["exc"], str)
|
||||
self.assertIsInstance(eval(response.json()["exc"]), list)
|
||||
self.assertIn("exc", response.json)
|
||||
self.assertIsInstance(response.json["exc"], str)
|
||||
self.assertIsInstance(eval(response.json["exc"]), list)
|
||||
|
||||
def test_get_list_fields(self):
|
||||
# test 6: fetch response with fields
|
||||
response = self.get(self.DOCTYPE, r'&fields=["description"]')
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "fields": '["description"]'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
json = frappe._dict(response.json())
|
||||
json = frappe._dict(response.json)
|
||||
self.assertIn("description", json.data[0])
|
||||
|
||||
def test_create_document(self):
|
||||
# test 7: POST method on /api/resource to create doc
|
||||
data = {"description": frappe.mock("paragraph")}
|
||||
response = self.post(self.DOCTYPE, data)
|
||||
data = {"description": frappe.mock("paragraph"), "sid": self.sid}
|
||||
response = self.post(f"/api/resource/{self.DOCTYPE}", data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
docname = response.json()["data"]["name"]
|
||||
docname = response.json["data"]["name"]
|
||||
self.assertIsInstance(docname, str)
|
||||
self.GENERATED_DOCUMENTS.append(docname)
|
||||
|
||||
def test_update_document(self):
|
||||
# test 8: PUT method on /api/resource to update doc
|
||||
generated_desc = frappe.mock("paragraph")
|
||||
data = {"description": generated_desc}
|
||||
data = {"description": generated_desc, "sid": self.sid}
|
||||
random_doc = choice(self.GENERATED_DOCUMENTS)
|
||||
desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description")
|
||||
|
||||
response = self.put(f"{self.DOCTYPE}/{random_doc}", data=data)
|
||||
response = self.put(f"/api/resource/{self.DOCTYPE}/{random_doc}", data=data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotEqual(response.json()["data"]["description"], desc_before_update)
|
||||
self.assertEqual(response.json()["data"]["description"], generated_desc)
|
||||
self.assertNotEqual(response.json["data"]["description"], desc_before_update)
|
||||
self.assertEqual(response.json["data"]["description"], generated_desc)
|
||||
|
||||
def test_delete_document(self):
|
||||
# test 9: DELETE method on /api/resource
|
||||
doc_to_delete = choice(self.GENERATED_DOCUMENTS)
|
||||
response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}")
|
||||
response = self.delete(f"/api/resource/{self.DOCTYPE}/{doc_to_delete}")
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertDictEqual(response.json(), {"message": "ok"})
|
||||
self.assertDictEqual(response.json, {"message": "ok"})
|
||||
self.GENERATED_DOCUMENTS.remove(doc_to_delete)
|
||||
|
||||
non_existent_doc = frappe.generate_hash(length=12)
|
||||
response = self.delete(f"{self.DOCTYPE}/{non_existent_doc}")
|
||||
with suppress_stdout():
|
||||
response = self.delete(f"/api/resource/{self.DOCTYPE}/{non_existent_doc}")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertDictEqual(response.json(), {})
|
||||
self.assertDictEqual(response.json, {})
|
||||
|
||||
def test_run_doc_method(self):
|
||||
# test 10: Run whitelisted method on doc via /api/resource
|
||||
# status_code is 403 if no other tests are run before this - it's not logged in
|
||||
self.post("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
|
||||
response = self.get("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
|
||||
self.assertIn(response.status_code, (403, 200))
|
||||
|
||||
if response.status_code == 403:
|
||||
self.assertTrue(set(response.json.keys()) == {'exc_type', 'exception', 'exc', '_server_messages'})
|
||||
self.assertEqual(response.json.get('exc_type'), 'PermissionError')
|
||||
self.assertEqual(response.json.get('exception'), 'frappe.exceptions.PermissionError: Not permitted')
|
||||
self.assertIsInstance(response.json.get('exc'), str)
|
||||
|
||||
elif response.status_code == 200:
|
||||
data = response.json.get("data")
|
||||
self.assertIsInstance(data, list)
|
||||
self.assertIsInstance(data[0], dict)
|
||||
|
||||
|
||||
class TestMethodAPI(unittest.TestCase):
|
||||
METHOD_URL = f"{get_site_url(frappe.local.site)}/api/method"
|
||||
class TestMethodAPI(FrappeAPITestCase):
|
||||
METHOD_PATH = "/api/method"
|
||||
|
||||
def test_version(self):
|
||||
# test 1: test for /api/method/version
|
||||
response = requests.get(f"{self.METHOD_URL}/version")
|
||||
json = frappe._dict(response.json())
|
||||
response = self.get(f"{self.METHOD_PATH}/version")
|
||||
json = frappe._dict(response.json)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(json, dict)
|
||||
|
|
@ -170,7 +238,27 @@ class TestMethodAPI(unittest.TestCase):
|
|||
|
||||
def test_ping(self):
|
||||
# test 2: test for /api/method/ping
|
||||
response = requests.get(f"{self.METHOD_URL}/ping")
|
||||
response = self.get(f"{self.METHOD_PATH}/ping")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json(), dict)
|
||||
self.assertEqual(response.json()['message'], "pong")
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertEqual(response.json["message"], "pong")
|
||||
|
||||
def test_get_user_info(self):
|
||||
# test 3: test for /api/method/frappe.realtime.get_user_info
|
||||
response = self.get(f"{self.METHOD_PATH}/frappe.realtime.get_user_info")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest"))
|
||||
|
||||
def test_auth_cycle(self):
|
||||
# test 4: Pass authorization token in request
|
||||
global authorization_token
|
||||
user = frappe.get_doc("User", "Administrator")
|
||||
api_key, api_secret = user.api_key, user.get_password("api_secret")
|
||||
authorization_token = f"{api_key}:{api_secret}"
|
||||
response = self.get("/api/method/frappe.auth.get_logged_user")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json["message"], "Administrator")
|
||||
|
||||
authorization_token = None
|
||||
|
|
|
|||
|
|
@ -3,25 +3,37 @@
|
|||
|
||||
# imports - standard imports
|
||||
import gzip
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import List
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from glob import glob
|
||||
from typing import List, Optional
|
||||
from unittest.case import skipIf
|
||||
from unittest.mock import patch
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
from click.testing import CliRunner, Result
|
||||
from click import Command
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
import frappe.commands.site
|
||||
import frappe.commands.utils
|
||||
import frappe.recorder
|
||||
from frappe.installer import add_to_installed_apps, remove_app
|
||||
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
|
||||
from frappe.utils.backups import fetch_latest_backups
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
_result: Optional[Result] = None
|
||||
TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
|
||||
CLI_CONTEXT = frappe._dict(sites=[TEST_SITE])
|
||||
|
||||
|
||||
def clean(value) -> str:
|
||||
|
|
@ -76,7 +88,61 @@ def exists_in_backup(doctypes: List, file: os.PathLike) -> bool:
|
|||
return len(missing_doctypes) == 0
|
||||
|
||||
|
||||
@contextmanager
|
||||
def maintain_locals():
|
||||
pre_site = frappe.local.site
|
||||
pre_flags = frappe.local.flags.copy()
|
||||
pre_db = frappe.local.db
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
post_site = getattr(frappe.local, "site", None)
|
||||
if not post_site or post_site != pre_site:
|
||||
frappe.init(site=pre_site)
|
||||
frappe.local.db = pre_db
|
||||
frappe.local.flags.update(pre_flags)
|
||||
|
||||
|
||||
def pass_test_context(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
return f(CLI_CONTEXT, *args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cli(cmd: Command, args: Optional[List] = None):
|
||||
with maintain_locals():
|
||||
global _result
|
||||
|
||||
patch_ctx = patch("frappe.commands.pass_context", pass_test_context)
|
||||
_module = cmd.callback.__module__
|
||||
_cmd = cmd.callback.__qualname__
|
||||
|
||||
__module = importlib.import_module(_module)
|
||||
patch_ctx.start()
|
||||
importlib.reload(__module)
|
||||
click_cmd = getattr(__module, _cmd)
|
||||
|
||||
try:
|
||||
_result = CliRunner().invoke(click_cmd, args=args)
|
||||
_result.command = str(cmd)
|
||||
yield _result
|
||||
finally:
|
||||
patch_ctx.stop()
|
||||
__module = importlib.import_module(_module)
|
||||
importlib.reload(__module)
|
||||
importlib.invalidate_caches()
|
||||
|
||||
|
||||
class BaseTestCommands(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.setup_test_site()
|
||||
return super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def execute(self, command, kwargs=None):
|
||||
site = {"site": frappe.local.site}
|
||||
cmd_input = None
|
||||
|
|
@ -102,16 +168,48 @@ class BaseTestCommands(unittest.TestCase):
|
|||
self.stderr = clean(self._proc.stderr)
|
||||
self.returncode = clean(self._proc.returncode)
|
||||
|
||||
@classmethod
|
||||
def setup_test_site(cls):
|
||||
cmd_config = {
|
||||
"test_site": TEST_SITE,
|
||||
"admin_password": frappe.conf.admin_password,
|
||||
"root_login": frappe.conf.root_login,
|
||||
"root_password": frappe.conf.root_password,
|
||||
"db_type": frappe.conf.db_type,
|
||||
}
|
||||
|
||||
if not os.path.exists(
|
||||
os.path.join(TEST_SITE, "site_config.json")
|
||||
):
|
||||
cls.execute(
|
||||
"bench new-site {test_site} --admin-password {admin_password} --db-type"
|
||||
" {db_type}",
|
||||
cmd_config,
|
||||
)
|
||||
|
||||
def _formatMessage(self, msg, standardMsg):
|
||||
output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)
|
||||
|
||||
if not hasattr(self, "command") and _result:
|
||||
command = _result.command
|
||||
stdout = _result.stdout_bytes.decode() if _result.stdout_bytes else None
|
||||
stderr = _result.stderr_bytes.decode() if _result.stderr_bytes else None
|
||||
returncode = _result.exit_code
|
||||
else:
|
||||
command = self.command
|
||||
stdout = self.stdout
|
||||
stderr = self.stderr
|
||||
returncode = self.returncode
|
||||
|
||||
cmd_execution_summary = "\n".join([
|
||||
"-" * 70,
|
||||
"Last Command Execution Summary:",
|
||||
"Command: {}".format(self.command) if self.command else "",
|
||||
"Standard Output: {}".format(self.stdout) if self.stdout else "",
|
||||
"Standard Error: {}".format(self.stderr) if self.stderr else "",
|
||||
"Return Code: {}".format(self.returncode) if self.returncode else "",
|
||||
"Command: {}".format(command) if command else "",
|
||||
"Standard Output: {}".format(stdout) if stdout else "",
|
||||
"Standard Error: {}".format(stderr) if stderr else "",
|
||||
"Return Code: {}".format(returncode) if returncode else "",
|
||||
]).strip()
|
||||
|
||||
return "{}\n\n{}".format(output, cmd_execution_summary)
|
||||
|
||||
|
||||
|
|
@ -135,6 +233,7 @@ class TestCommands(BaseTestCommands):
|
|||
self.assertEqual(self.returncode, 0)
|
||||
self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))
|
||||
|
||||
@unittest.skip
|
||||
def test_restore(self):
|
||||
# step 0: create a site to run the test on
|
||||
global_config = {
|
||||
|
|
@ -143,35 +242,30 @@ class TestCommands(BaseTestCommands):
|
|||
"root_password": frappe.conf.root_password,
|
||||
"db_type": frappe.conf.db_type,
|
||||
}
|
||||
site_data = {"another_site": f"{frappe.local.site}-restore.test", **global_config}
|
||||
site_data = {"test_site": TEST_SITE, **global_config}
|
||||
for key, value in global_config.items():
|
||||
if value:
|
||||
self.execute(f"bench set-config {key} {value} -g")
|
||||
self.execute(
|
||||
"bench new-site {another_site} --admin-password {admin_password} --db-type"
|
||||
" {db_type}",
|
||||
site_data,
|
||||
)
|
||||
|
||||
# test 1: bench restore from full backup
|
||||
self.execute("bench --site {another_site} backup --ignore-backup-conf", site_data)
|
||||
self.execute("bench --site {test_site} backup --ignore-backup-conf", site_data)
|
||||
self.execute(
|
||||
"bench --site {another_site} execute frappe.utils.backups.fetch_latest_backups",
|
||||
"bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups",
|
||||
site_data,
|
||||
)
|
||||
site_data.update({"database": json.loads(self.stdout)["database"]})
|
||||
self.execute("bench --site {another_site} restore {database}", site_data)
|
||||
self.execute("bench --site {test_site} restore {database}", site_data)
|
||||
|
||||
# test 2: restore from partial backup
|
||||
self.execute("bench --site {another_site} backup --exclude 'ToDo'", site_data)
|
||||
self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
|
||||
site_data.update({"kw": "\"{'partial':True}\""})
|
||||
self.execute(
|
||||
"bench --site {another_site} execute"
|
||||
"bench --site {test_site} execute"
|
||||
" frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
|
||||
site_data,
|
||||
)
|
||||
site_data.update({"database": json.loads(self.stdout)["database"]})
|
||||
self.execute("bench --site {another_site} restore {database}", site_data)
|
||||
self.execute("bench --site {test_site} restore {database}", site_data)
|
||||
self.assertEqual(self.returncode, 1)
|
||||
|
||||
def test_partial_restore(self):
|
||||
|
|
@ -226,7 +320,8 @@ class TestCommands(BaseTestCommands):
|
|||
def test_list_apps(self):
|
||||
# test 1: sanity check for command
|
||||
self.execute("bench --site all list-apps")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertIsNotNone(self.returncode)
|
||||
self.assertIsInstance(self.stdout or self.stderr, str)
|
||||
|
||||
# test 2: bare functionality for single site
|
||||
self.execute("bench --site {site} list-apps")
|
||||
|
|
@ -242,14 +337,12 @@ class TestCommands(BaseTestCommands):
|
|||
self.assertSetEqual(list_apps, installed_apps)
|
||||
|
||||
# test 3: parse json format
|
||||
self.execute("bench --site all list-apps --format json")
|
||||
self.execute("bench --site {site} list-apps --format json")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} list-apps --format json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} list-apps -f json")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
def test_show_config(self):
|
||||
|
|
@ -358,7 +451,7 @@ class TestCommands(BaseTestCommands):
|
|||
)
|
||||
def test_bench_drop_site_should_archive_site(self):
|
||||
# TODO: Make this test postgres compatible
|
||||
site = 'test_site.localhost'
|
||||
site = TEST_SITE
|
||||
|
||||
self.execute(
|
||||
f"bench new-site {site} --force --verbose "
|
||||
|
|
@ -585,3 +678,18 @@ class TestRemoveApp(unittest.TestCase):
|
|||
|
||||
# nothing to assert, if this fails rest of the test suite will crumble.
|
||||
remove_app("frappe", dry_run=True, yes=True, no_backup=True)
|
||||
|
||||
|
||||
class TestSiteMigration(BaseTestCommands):
|
||||
def test_migrate_cli(self):
|
||||
with cli(frappe.commands.site.migrate) as result:
|
||||
self.assertTrue(TEST_SITE in result.stdout)
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
self.assertEqual(result.exception, None)
|
||||
|
||||
|
||||
class TestBenchBuild(BaseTestCommands):
|
||||
def test_build_assets(self):
|
||||
with cli(frappe.commands.utils.build) as result:
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
self.assertEqual(result.exception, None)
|
||||
|
|
|
|||
|
|
@ -291,6 +291,16 @@ class TestDB(unittest.TestCase):
|
|||
|
||||
frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION
|
||||
|
||||
def test_pk_collision_ignoring(self):
|
||||
# note has `name` generated from title
|
||||
for _ in range(3):
|
||||
frappe.get_doc(doctype="Note", title="duplicate name").insert(ignore_if_duplicate=True)
|
||||
|
||||
with savepoint():
|
||||
self.assertRaises(frappe.DuplicateEntryError, frappe.get_doc(doctype="Note", title="duplicate name").insert)
|
||||
# recover transaction to continue other tests
|
||||
raise Exception
|
||||
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
class TestDDLCommandsMaria(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
|
||||
from frappe.desk.doctype.note.note import Note
|
||||
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
|
||||
from frappe.utils import cint, now_datetime
|
||||
|
||||
|
||||
class CustomTestNote(Note):
|
||||
@property
|
||||
def age(self):
|
||||
return now_datetime() - self.creation
|
||||
|
||||
|
||||
class TestDocument(unittest.TestCase):
|
||||
|
|
@ -255,5 +264,58 @@ class TestDocument(unittest.TestCase):
|
|||
|
||||
def test_limit_for_get(self):
|
||||
doc = frappe.get_doc("DocType", "DocType")
|
||||
# assuming DocType has more that 3 Data fields
|
||||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
|
||||
# assuming DocType has more than 3 Data fields
|
||||
self.assertEquals(len(doc.get("fields", limit=3)), 3)
|
||||
|
||||
# limit with filters
|
||||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
|
||||
|
||||
def test_virtual_fields(self):
|
||||
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked
|
||||
"""
|
||||
frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"})
|
||||
note = frappe.new_doc("Note")
|
||||
note.content = "some content"
|
||||
note.title = frappe.generate_hash(length=20)
|
||||
note.insert()
|
||||
|
||||
def patch_note():
|
||||
return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}})
|
||||
|
||||
@contextmanager
|
||||
def customize_note(with_options=False):
|
||||
options = "frappe.utils.now_datetime() - doc.creation" if with_options else ""
|
||||
custom_field = frappe.get_doc({
|
||||
"doctype": "Custom Field",
|
||||
"dt": "Note",
|
||||
"fieldname": "age",
|
||||
"fieldtype": "Data",
|
||||
"read_only": True,
|
||||
"is_virtual": True,
|
||||
"options": options,
|
||||
})
|
||||
|
||||
try:
|
||||
yield custom_field.insert(ignore_if_duplicate=True)
|
||||
finally:
|
||||
custom_field.delete()
|
||||
|
||||
with patch_note():
|
||||
doc = frappe.get_last_doc("Note")
|
||||
self.assertIsInstance(doc, CustomTestNote)
|
||||
self.assertIsInstance(doc.age, timedelta)
|
||||
self.assertIsNone(doc.as_dict().get("age"))
|
||||
self.assertIsNone(doc.get_valid_dict().get("age"))
|
||||
|
||||
with customize_note(), patch_note():
|
||||
doc = frappe.get_last_doc("Note")
|
||||
self.assertIsInstance(doc, CustomTestNote)
|
||||
self.assertIsInstance(doc.age, timedelta)
|
||||
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
|
||||
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
|
||||
|
||||
with customize_note(with_options=True):
|
||||
doc = frappe.get_last_doc("Note")
|
||||
self.assertIsInstance(doc, Note)
|
||||
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
|
||||
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import requests
|
|||
import base64
|
||||
|
||||
class TestFrappeClient(unittest.TestCase):
|
||||
PASSWORD = "admin"
|
||||
PASSWORD = frappe.conf.admin_password or "admin"
|
||||
|
||||
def test_insert_many(self):
|
||||
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
|
||||
frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))})
|
||||
|
|
@ -169,7 +170,6 @@ class TestFrappeClient(unittest.TestCase):
|
|||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
self.assertEqual(res.status_code, 403)
|
||||
|
||||
|
||||
# random api key and api secret
|
||||
api_key = "@3djdk3kld"
|
||||
api_secret = "ksk&93nxoe3os"
|
||||
|
|
|
|||
|
|
@ -419,3 +419,96 @@ class TestXlsxUtils(unittest.TestCase):
|
|||
val = handle_html("<p>html data ></p>")
|
||||
self.assertIn("html data >", val)
|
||||
self.assertEqual("abc", handle_html("abc"))
|
||||
|
||||
|
||||
class TestLinkTitle(unittest.TestCase):
|
||||
def test_link_title_doctypes_in_boot_info(self):
|
||||
"""
|
||||
Test that doctypes are added to link_title_map in boot_info
|
||||
"""
|
||||
custom_doctype = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{
|
||||
"label": "Test Field",
|
||||
"fieldname": "test_title_field",
|
||||
"fieldtype": "Data",
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
"title_field": "test_title_field",
|
||||
"permissions": [{"role": "System Manager", "read": 1}],
|
||||
"name": "Test Custom Doctype for Link Title",
|
||||
}
|
||||
)
|
||||
custom_doctype.insert()
|
||||
|
||||
prop_setter = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doc_type": "User",
|
||||
"property": "show_title_field_in_link",
|
||||
"property_type": "Check",
|
||||
"doctype_or_field": "DocType",
|
||||
"value": "1",
|
||||
}
|
||||
).insert()
|
||||
|
||||
from frappe.boot import get_link_title_doctypes
|
||||
|
||||
link_title_doctypes = get_link_title_doctypes()
|
||||
self.assertTrue("User" in link_title_doctypes)
|
||||
self.assertTrue("Test Custom Doctype for Link Title" in link_title_doctypes)
|
||||
|
||||
prop_setter.delete()
|
||||
custom_doctype.delete()
|
||||
|
||||
def test_link_titles_on_getdoc(self):
|
||||
"""
|
||||
Test that link titles are added to the doctype on getdoc
|
||||
"""
|
||||
prop_setter = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doc_type": "User",
|
||||
"property": "show_title_field_in_link",
|
||||
"property_type": "Check",
|
||||
"doctype_or_field": "DocType",
|
||||
"value": "1",
|
||||
}
|
||||
).insert()
|
||||
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"user_type": "Website User",
|
||||
"email": "test_user_for_link_title@example.com",
|
||||
"send_welcome_email": 0,
|
||||
"first_name": "Test User for Link Title",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
todo = frappe.get_doc(
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"description": "test-link-title-on-getdoc",
|
||||
"allocated_to": user.name,
|
||||
}
|
||||
).insert()
|
||||
|
||||
from frappe.desk.form.load import getdoc
|
||||
|
||||
getdoc("ToDo", todo.name)
|
||||
link_titles = frappe.local.response["_link_titles"]
|
||||
|
||||
self.assertTrue(f"{user.doctype}::{user.name}" in link_titles)
|
||||
self.assertEqual(link_titles[f"{user.doctype}::{user.name}"], user.full_name)
|
||||
|
||||
todo.delete()
|
||||
user.delete()
|
||||
prop_setter.delete()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ def create_contact_records():
|
|||
insert_contact('Test Form Contact 2', '54321')
|
||||
insert_contact('Test Form Contact 3', '12345')
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_multiple_contact_records():
|
||||
if frappe.db.get_all('Contact', {'first_name': 'Multiple Contact 1'}):
|
||||
return
|
||||
for index in range(1001):
|
||||
insert_contact('Multiple Contact {}'.format(index+1), '12345{}'.format(index+1))
|
||||
|
||||
def insert_contact(first_name, phone_number):
|
||||
doc = frappe.get_doc({
|
||||
|
|
@ -249,3 +255,17 @@ def update_webform_to_multistep():
|
|||
_doc.route = "update-profile-duplicate"
|
||||
_doc.is_standard = False
|
||||
_doc.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_table(name):
|
||||
doc = frappe.get_doc('DocType', name)
|
||||
if len(doc.fields) == 1:
|
||||
doc.append('fields', {
|
||||
'fieldname': 'doctype_to_link',
|
||||
'fieldtype': 'Link',
|
||||
'in_list_view': 1,
|
||||
'label': 'Doctype to Link',
|
||||
'options': 'Doctype to Link'
|
||||
})
|
||||
|
||||
doc.save()
|
||||
|
|
@ -148,6 +148,8 @@ More Information,Mehr Informationen,
|
|||
More...,Mehr...,
|
||||
Move,Bewegen,
|
||||
My Account,Mein Konto,
|
||||
My Profile,Mein Profil,
|
||||
My Settings,Meine Einstellungen,
|
||||
New Address,Neue Adresse,
|
||||
New Contact,Neuer Kontakt,
|
||||
Next,Weiter,
|
||||
|
|
@ -406,7 +408,7 @@ Allow Self Approval,Erlaube Selbstgenehmigung,
|
|||
Allow approval for creator of the document,Genehmigung für den Ersteller des Dokuments zulassen,
|
||||
Allow events in timeline,Ereignisse in der Zeitleiste zulassen,
|
||||
Allow in Quick Entry,In Schnelleingabe zulassen,
|
||||
Allow on Submit,Beim Übertragen zulassen,
|
||||
Allow on Submit,Änderungen zulassen wenn gebucht,
|
||||
Allow only one session per user,Nur eine Sitzung pro Benutzer zulassen,
|
||||
Allow page break inside tables,Seitenumbruch innerhalb von Tabellen erlauben,
|
||||
Allow saving if mandatory fields are not filled,Speichern trotz leerer Pflichtfelder zulassen,
|
||||
|
|
@ -734,6 +736,7 @@ Content Hash,Inhalts-Hash,
|
|||
Content web page.,Inhalt der Webseite.,
|
||||
Conversation Tones,Konversationstöne,
|
||||
Copyright,Copyright,
|
||||
Copy to Clipboard,In die Zwischenablage,
|
||||
Core,Kern,
|
||||
Core DocTypes cannot be customized.,Core DocTypes können nicht angepasst werden.,
|
||||
Could not connect to outgoing email server,Konnte keine Verbindung zum Postausgangsserver herstellen,
|
||||
|
|
@ -957,6 +960,7 @@ Edit {0},Bearbeiten {0},
|
|||
Editable Grid,Editierbares Raster,
|
||||
Editing Row,Zeile bearbeiten,
|
||||
Eg. smsgateway.com/api/send_sms.cgi,z. B. smsgateway.com/api/send_sms.cgi,
|
||||
Email,E-Mail,
|
||||
Email Account Name,E-Mail-Konten-Name,
|
||||
Email Account added multiple times,E-Mail-Konto wurde mehrmals hinzugefügt,
|
||||
Email Addresses,E-Mail-Adressen,
|
||||
|
|
@ -1222,8 +1226,8 @@ Headers,Headers,
|
|||
Heading,Überschrift,
|
||||
Hello {0},Hallo {0},
|
||||
Hello!,Hallo!,
|
||||
Help Articles,Artikel-Hilfe,
|
||||
Help Category,Kategorie-Hilfe,
|
||||
Help Articles,Hilfeartikel,
|
||||
Help Category,Hilfekategorie,
|
||||
Help on Search,Hilfe zur Suche,
|
||||
"Help: To link to another record in the system, use ""#Form/Note/[Note Name]"" as the Link URL. (don't use ""http://"")","Hilfe: Um eine Verknüpfung mit einem anderen Datensatz im System zu erstellen, bitte ""#Formular/Anmerkung/[Anmerkungsname]"" als Verknüpfungs-URL verwenden (kein ""http://""!).",
|
||||
Helvetica,Helvetica,
|
||||
|
|
@ -1451,6 +1455,7 @@ Last User,Letzter Benutzer,
|
|||
Last Week,Letzte Woche,
|
||||
Last Year,Vergangenes Jahr,
|
||||
Last synced {0},Zuletzt synchronisiert {0},
|
||||
Learn more,Mehr erfahren,
|
||||
Leave a Comment,Hinterlasse einen Kommentar,
|
||||
Leave blank to repeat always,"Freilassen, um immer zu wiederholen",
|
||||
Leave this conversation,Benachrichtigungen abbestellen,
|
||||
|
|
@ -1483,7 +1488,8 @@ Linked with {0},Verknüpft mit {0},
|
|||
Links,Verknüpfungen,
|
||||
List,Listenansicht,
|
||||
List Filter,Listenfilter,
|
||||
List View Setting,List View Setting,
|
||||
List View,Listenansicht,
|
||||
List View Setting,Einstellungen zu Listenansicht,
|
||||
List a document type,Einen Dokumenttyp auflisten,
|
||||
"List as [{""label"": _(""Jobs""), ""route"":""jobs""}]","Liste als [{ ""label"": _ ( ""Jobs""), ""route"": ""jobs""}]",
|
||||
List of backups available for download,Datensicherungen herunterladen,
|
||||
|
|
|
|||
|
|
|
@ -438,7 +438,8 @@ def touch_file(path):
|
|||
os.utime(path, None)
|
||||
return path
|
||||
|
||||
def get_test_client():
|
||||
def get_test_client() -> Client:
|
||||
"""Returns an test instance of the Frappe WSGI"""
|
||||
from frappe.app import application
|
||||
return Client(application)
|
||||
|
||||
|
|
|
|||
|
|
@ -653,7 +653,8 @@ def get_backup_path():
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_backup_encryption_key():
|
||||
return frappe.local.conf.encryption_key
|
||||
frappe.only_for("System Manager")
|
||||
return frappe.conf.encryption_key
|
||||
|
||||
class Backup:
|
||||
def __init__(self, file_path):
|
||||
|
|
|
|||
|
|
@ -88,9 +88,14 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
|
|||
return frappe.utils.markdown(value)
|
||||
|
||||
elif df.get("fieldtype") == "Table MultiSelect":
|
||||
values = []
|
||||
meta = frappe.get_meta(df.options)
|
||||
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0]
|
||||
values = [v.get(link_field.fieldname, 'asdf') for v in value]
|
||||
for v in value:
|
||||
v.update({'__link_titles': doc.get('__link_titles')})
|
||||
formatted_value = frappe.format_value(v.get(link_field.fieldname, ''), link_field, v)
|
||||
values.append(formatted_value)
|
||||
|
||||
return ', '.join(values)
|
||||
|
||||
elif df.get("fieldtype") == "Duration":
|
||||
|
|
@ -100,4 +105,19 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
|
|||
elif df.get("fieldtype") == "Text Editor":
|
||||
return "<div class='ql-snow'>{}</div>".format(value)
|
||||
|
||||
elif df.get("fieldtype") in ["Link", "Dynamic Link"]:
|
||||
if not doc or not doc.get("__link_titles") or not df.options:
|
||||
return value
|
||||
|
||||
doctype = df.options
|
||||
if df.get("fieldtype") == "Dynamic Link":
|
||||
if not df.parent:
|
||||
return value
|
||||
|
||||
meta = frappe.get_meta(df.parent)
|
||||
_field = meta.get_field(df.options)
|
||||
doctype = _field.options
|
||||
|
||||
return doc.__link_titles.get("{0}::{1}".format(doctype, value), value)
|
||||
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
|
||||
# This code is original from jsmin by Douglas Crockford, it was translated to
|
||||
# Python by Baruch Even. The original code had the following copyright and
|
||||
# license.
|
||||
#
|
||||
# /* jsmin.c
|
||||
# 2007-05-22
|
||||
#
|
||||
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
# this software and associated documentation files (the "Software"), to deal in
|
||||
# the Software without restriction, including without limitation the rights to
|
||||
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software is furnished to do
|
||||
# so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# The Software shall be used for Good, not Evil.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# */
|
||||
|
||||
from io import StringIO
|
||||
|
||||
def jsmin(js):
|
||||
ins = StringIO(js)
|
||||
outs = StringIO()
|
||||
JavascriptMinify().minify(ins, outs)
|
||||
str = outs.getvalue()
|
||||
if len(str) > 0 and str[0] == '\n':
|
||||
str = str[1:]
|
||||
return str
|
||||
|
||||
def isAlphanum(c):
|
||||
"""return true if the character is a letter, digit, underscore,
|
||||
dollar sign, or non-ASCII character.
|
||||
"""
|
||||
return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
|
||||
(c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
|
||||
|
||||
class UnterminatedComment(Exception):
|
||||
pass
|
||||
|
||||
class UnterminatedStringLiteral(Exception):
|
||||
pass
|
||||
|
||||
class UnterminatedRegularExpression(Exception):
|
||||
pass
|
||||
|
||||
class JavascriptMinify(object):
|
||||
|
||||
def _outA(self):
|
||||
self.outstream.write(self.theA)
|
||||
def _outB(self):
|
||||
self.outstream.write(self.theB)
|
||||
|
||||
def _get(self):
|
||||
"""return the next character from stdin. Watch out for lookahead. If
|
||||
the character is a control character, translate it to a space or
|
||||
linefeed.
|
||||
"""
|
||||
c = self.theLookahead
|
||||
self.theLookahead = None
|
||||
if c is None:
|
||||
c = self.instream.read(1)
|
||||
if c >= ' ' or c == '\n':
|
||||
return c
|
||||
if c == '': # EOF
|
||||
return '\000'
|
||||
if c == '\r':
|
||||
return '\n'
|
||||
return ' '
|
||||
|
||||
def _peek(self):
|
||||
self.theLookahead = self._get()
|
||||
return self.theLookahead
|
||||
|
||||
def _next(self):
|
||||
"""get the next character, excluding comments. peek() is used to see
|
||||
if an unescaped '/' is followed by a '/' or '*'.
|
||||
"""
|
||||
c = self._get()
|
||||
if c == '/' and self.theA != '\\':
|
||||
p = self._peek()
|
||||
if p == '/':
|
||||
c = self._get()
|
||||
while c > '\n':
|
||||
c = self._get()
|
||||
return c
|
||||
if p == '*':
|
||||
c = self._get()
|
||||
while 1:
|
||||
c = self._get()
|
||||
if c == '*':
|
||||
if self._peek() == '/':
|
||||
self._get()
|
||||
return ' '
|
||||
if c == '\000':
|
||||
raise UnterminatedComment()
|
||||
|
||||
return c
|
||||
|
||||
def _action(self, action):
|
||||
"""do something! What you do is determined by the argument:
|
||||
1 Output A. Copy B to A. Get the next B.
|
||||
2 Copy B to A. Get the next B. (Delete A).
|
||||
3 Get the next B. (Delete B).
|
||||
action treats a string as a single character. Wow!
|
||||
action recognizes a regular expression if it is preceded by ( or , or =.
|
||||
"""
|
||||
if action <= 1:
|
||||
self._outA()
|
||||
|
||||
if action <= 2:
|
||||
self.theA = self.theB
|
||||
if self.theA == "'" or self.theA == '"':
|
||||
while 1:
|
||||
self._outA()
|
||||
self.theA = self._get()
|
||||
if self.theA == self.theB:
|
||||
break
|
||||
if self.theA <= '\n':
|
||||
raise UnterminatedStringLiteral()
|
||||
if self.theA == '\\':
|
||||
self._outA()
|
||||
self.theA = self._get()
|
||||
|
||||
|
||||
if action <= 3:
|
||||
self.theB = self._next()
|
||||
if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
|
||||
self.theA == '=' or self.theA == ':' or
|
||||
self.theA == '[' or self.theA == '?' or
|
||||
self.theA == '!' or self.theA == '&' or
|
||||
self.theA == '|' or self.theA == ';' or
|
||||
self.theA == '{' or self.theA == '}' or
|
||||
self.theA == '\n'):
|
||||
self._outA()
|
||||
self._outB()
|
||||
while 1:
|
||||
self.theA = self._get()
|
||||
if self.theA == '/':
|
||||
break
|
||||
elif self.theA == '\\':
|
||||
self._outA()
|
||||
self.theA = self._get()
|
||||
elif self.theA <= '\n':
|
||||
raise UnterminatedRegularExpression()
|
||||
self._outA()
|
||||
self.theB = self._next()
|
||||
|
||||
|
||||
def _jsmin(self):
|
||||
"""Copy the input to the output, deleting the characters which are
|
||||
insignificant to JavaScript. Comments will be removed. Tabs will be
|
||||
replaced with spaces. Carriage returns will be replaced with linefeeds.
|
||||
Most spaces and linefeeds will be removed.
|
||||
"""
|
||||
self.theA = '\n'
|
||||
self._action(3)
|
||||
|
||||
while self.theA != '\000':
|
||||
if self.theA == ' ':
|
||||
if isAlphanum(self.theB):
|
||||
self._action(1)
|
||||
else:
|
||||
self._action(2)
|
||||
elif self.theA == '\n':
|
||||
if self.theB in ['{', '[', '(', '+', '-']:
|
||||
self._action(1)
|
||||
elif self.theB == ' ':
|
||||
self._action(3)
|
||||
else:
|
||||
if isAlphanum(self.theB):
|
||||
self._action(1)
|
||||
else:
|
||||
self._action(2)
|
||||
else:
|
||||
if self.theB == ' ':
|
||||
if isAlphanum(self.theA):
|
||||
self._action(1)
|
||||
else:
|
||||
self._action(3)
|
||||
elif self.theB == '\n':
|
||||
if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
|
||||
self._action(1)
|
||||
else:
|
||||
if isAlphanum(self.theA):
|
||||
self._action(1)
|
||||
else:
|
||||
self._action(3)
|
||||
else:
|
||||
self._action(1)
|
||||
|
||||
def minify(self, instream, outstream):
|
||||
self.instream = instream
|
||||
self.outstream = outstream
|
||||
self.theA = '\n'
|
||||
self.theB = None
|
||||
self.theLookahead = None
|
||||
|
||||
self._jsmin()
|
||||
self.instream.close()
|
||||
|
|
@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", {
|
|||
options: field.options,
|
||||
reqd: field.reqd,
|
||||
default: field.default,
|
||||
read_only: field.read_only,
|
||||
read_only: field.read_only || field.is_virtual,
|
||||
depends_on: field.depends_on,
|
||||
mandatory_depends_on: field.mandatory_depends_on,
|
||||
read_only_depends_on: field.read_only_depends_on,
|
||||
|
|
|
|||
|
|
@ -151,7 +151,12 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
|
|||
|
||||
convert_markdown(doc, meta)
|
||||
|
||||
args = {
|
||||
args = {}
|
||||
# extract `print_heading_template` from the first field and remove it
|
||||
if format_data and format_data[0].get("fieldname") == "print_heading_template":
|
||||
args["print_heading_template"] = format_data.pop(0).get("options")
|
||||
|
||||
args.update({
|
||||
"doc": doc,
|
||||
"meta": frappe.get_meta(doc.doctype),
|
||||
"layout": make_layout(doc, meta, format_data),
|
||||
|
|
@ -160,7 +165,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
|
|||
"letter_head": letter_head.content,
|
||||
"footer": letter_head.footer,
|
||||
"print_settings": print_settings
|
||||
}
|
||||
})
|
||||
|
||||
html = template.render(args, filters={"len": len})
|
||||
|
||||
|
|
@ -169,6 +174,48 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
|
|||
|
||||
return html
|
||||
|
||||
def set_link_titles(doc):
|
||||
# Adds name with title of link field doctype to __link_titles
|
||||
if not doc.get("__link_titles"):
|
||||
setattr(doc, "__link_titles", {})
|
||||
|
||||
meta = frappe.get_meta(doc.doctype)
|
||||
set_title_values_for_link_and_dynamic_link_fields(meta, doc)
|
||||
set_title_values_for_table_and_multiselect_fields(meta, doc)
|
||||
|
||||
def set_title_values_for_link_and_dynamic_link_fields(meta, doc, parent_doc=None):
|
||||
if parent_doc and not parent_doc.get("__link_titles"):
|
||||
setattr(parent_doc, "__link_titles", {})
|
||||
elif doc and not doc.get("__link_titles"):
|
||||
setattr(doc, "__link_titles", {})
|
||||
|
||||
for field in meta.get_link_fields() + meta.get_dynamic_link_fields():
|
||||
if not doc.get(field.fieldname):
|
||||
continue
|
||||
|
||||
# If link field, then get doctype from options
|
||||
# If dynamic link field, then get doctype from dependent field
|
||||
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
if not meta or not (meta.title_field and meta.show_title_field_in_link):
|
||||
continue
|
||||
|
||||
link_title = frappe.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field)
|
||||
if parent_doc:
|
||||
parent_doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
|
||||
elif doc:
|
||||
doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
|
||||
|
||||
def set_title_values_for_table_and_multiselect_fields(meta, doc):
|
||||
for field in meta.get_table_fields():
|
||||
if not doc.get(field.fieldname):
|
||||
continue
|
||||
|
||||
_meta = frappe.get_meta(field.options)
|
||||
for value in doc.get(field.fieldname):
|
||||
set_title_values_for_link_and_dynamic_link_fields(_meta, value, doc)
|
||||
|
||||
def convert_markdown(doc, meta):
|
||||
'''Convert text field values to markdown if necessary'''
|
||||
for field in meta.fields:
|
||||
|
|
@ -190,6 +237,7 @@ def get_html_and_style(doc, name=None, print_format=None, meta=None,
|
|||
doc = frappe.get_doc(json.loads(doc))
|
||||
|
||||
print_format = get_print_format_doc(print_format, meta=meta or frappe.get_meta(doc.doctype))
|
||||
set_link_titles(doc)
|
||||
|
||||
try:
|
||||
html = get_rendered_template(doc, name=name, print_format=print_format, meta=meta,
|
||||
|
|
@ -276,13 +324,6 @@ def make_layout(doc, meta, format_data=None):
|
|||
layout, page = [], []
|
||||
layout.append(page)
|
||||
|
||||
if format_data:
|
||||
# extract print_heading_template from the first field
|
||||
# and remove the field
|
||||
if format_data[0].get("fieldname") == "print_heading_template":
|
||||
doc.print_heading_template = format_data[0].get("options")
|
||||
format_data = format_data[1:]
|
||||
|
||||
def get_new_section(): return {'columns': [], 'has_data': False}
|
||||
|
||||
def append_empty_field_dict_to_page_column(page):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue