@@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then | |||||
fi | fi | ||||
if [ "$DB" == "mariadb" ];then | if [ "$DB" == "mariadb" ];then | ||||
sudo apt install mariadb-client-10.3 | |||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; | ||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; | ||||
@@ -58,4 +59,4 @@ cd ../.. | |||||
bench start & | bench start & | ||||
bench --site test_site reinstall --yes | bench --site test_site reinstall --yes | ||||
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi | if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi | ||||
bench build --app frappe | |||||
bench build --app frappe |
@@ -12,7 +12,7 @@ jobs: | |||||
- name: 'Setup Environment' | - name: 'Setup Environment' | ||||
uses: actions/setup-python@v2 | uses: actions/setup-python@v2 | ||||
with: | with: | ||||
python-version: 3.6 | |||||
python-version: 3.7 | |||||
- name: 'Clone repo' | - name: 'Clone repo' | ||||
uses: actions/checkout@v2 | uses: actions/checkout@v2 | ||||
@@ -9,7 +9,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
name: Patch Test | name: Patch Test | ||||
@@ -18,7 +18,7 @@ jobs: | |||||
node-version: 14 | node-version: 14 | ||||
- uses: actions/setup-python@v2 | - uses: actions/setup-python@v2 | ||||
with: | with: | ||||
python-version: '3.6' | |||||
python-version: '3.7' | |||||
- name: Set up bench and build assets | - name: Set up bench and build assets | ||||
run: | | run: | | ||||
npm install -g yarn | npm install -g yarn | ||||
@@ -21,7 +21,7 @@ jobs: | |||||
python-version: '12.x' | python-version: '12.x' | ||||
- uses: actions/setup-python@v2 | - uses: actions/setup-python@v2 | ||||
with: | with: | ||||
python-version: '3.6' | |||||
python-version: '3.7' | |||||
- name: Set up bench and build assets | - name: Set up bench and build assets | ||||
run: | | run: | | ||||
npm install -g yarn | npm install -g yarn | ||||
@@ -13,7 +13,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
strategy: | strategy: | ||||
fail-fast: false | fail-fast: false | ||||
@@ -127,4 +127,4 @@ jobs: | |||||
name: MariaDB | name: MariaDB | ||||
fail_ci_if_error: true | fail_ci_if_error: true | ||||
files: /home/runner/frappe-bench/sites/coverage.xml | files: /home/runner/frappe-bench/sites/coverage.xml | ||||
verbose: true | |||||
verbose: true |
@@ -12,7 +12,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
strategy: | strategy: | ||||
fail-fast: false | fail-fast: false | ||||
@@ -1,22 +0,0 @@ | |||||
name: Frappe Linter | |||||
on: | |||||
pull_request: | |||||
branches: | |||||
- develop | |||||
- version-12-hotfix | |||||
- version-11-hotfix | |||||
jobs: | |||||
check_translation: | |||||
name: Translation Syntax Check | |||||
runs-on: ubuntu-18.04 | |||||
steps: | |||||
- uses: actions/checkout@v2 | |||||
- name: Setup python3 | |||||
uses: actions/setup-python@v1 | |||||
with: | |||||
python-version: 3.6 | |||||
- name: Validating Translation Syntax | |||||
run: | | |||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q | |||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) | |||||
python $GITHUB_WORKSPACE/.github/helper/translation.py $files |
@@ -12,7 +12,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
strategy: | strategy: | ||||
fail-fast: false | fail-fast: false | ||||
@@ -0,0 +1,59 @@ | |||||
export default { | |||||
name: 'Form With Tab Break', | |||||
custom: 1, | |||||
actions: [], | |||||
doctype: 'DocType', | |||||
engine: 'InnoDB', | |||||
fields: [ | |||||
{ | |||||
fieldname: 'username', | |||||
fieldtype: 'Data', | |||||
label: 'Name', | |||||
options: 'Name' | |||||
}, | |||||
{ | |||||
fieldname: 'tab', | |||||
fieldtype: 'Tab Break', | |||||
label: 'Tab 2', | |||||
}, | |||||
{ | |||||
fieldname: 'Phone', | |||||
fieldtype: 'Data', | |||||
label: 'Phone', | |||||
options: 'Phone', | |||||
reqd: 1 | |||||
}, | |||||
], | |||||
links: [ | |||||
{ | |||||
"group": "Profile", | |||||
"link_doctype": "Contact", | |||||
"link_fieldname": "user" | |||||
}, | |||||
{ | |||||
"group": "Profile", | |||||
"link_doctype": "Chat Profile", | |||||
"link_fieldname": "user" | |||||
}, | |||||
], | |||||
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 | |||||
} | |||||
], | |||||
quick_entry: 1, | |||||
autoname: "format: Test-{####}", | |||||
sort_field: 'modified', | |||||
sort_order: 'ASC', | |||||
track_changes: 1 | |||||
}; |
@@ -0,0 +1,93 @@ | |||||
context("Control Float", () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit("/app/website"); | |||||
}); | |||||
function get_dialog_with_float() { | |||||
return cy.dialog({ | |||||
title: "Float Check", | |||||
fields: [ | |||||
{ | |||||
fieldname: "float_number", | |||||
fieldtype: "Float", | |||||
Label: "Float" | |||||
} | |||||
] | |||||
}); | |||||
} | |||||
it("check value changes", () => { | |||||
get_dialog_with_float().as("dialog"); | |||||
let data = get_data(); | |||||
data.forEach(x => { | |||||
cy.window() | |||||
.its("frappe") | |||||
.then(frappe => { | |||||
frappe.boot.sysdefaults.number_format = x.number_format; | |||||
}); | |||||
x.values.forEach(d => { | |||||
cy.get_field("float_number", "Float").clear(); | |||||
cy.fill_field("float_number", d.input, "Float").blur(); | |||||
cy.get_field("float_number", "Float").should( | |||||
"have.value", | |||||
d.blur_expected | |||||
); | |||||
cy.get_field("float_number", "Float").focus(); | |||||
cy.get_field("float_number", "Float").blur(); | |||||
cy.get_field("float_number", "Float").focus(); | |||||
cy.get_field("float_number", "Float").should( | |||||
"have.value", | |||||
d.focus_expected | |||||
); | |||||
}); | |||||
}); | |||||
}); | |||||
function get_data() { | |||||
return [ | |||||
{ | |||||
number_format: "#.###,##", | |||||
values: [ | |||||
{ | |||||
input: "364.87,334", | |||||
blur_expected: "36.487,334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "36487,334", | |||||
blur_expected: "36.487,334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "100", | |||||
blur_expected: "100,000", | |||||
focus_expected: "100" | |||||
} | |||||
] | |||||
}, | |||||
{ | |||||
number_format: "#,###.##", | |||||
values: [ | |||||
{ | |||||
input: "364,87.334", | |||||
blur_expected: "36,487.334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "36487.334", | |||||
blur_expected: "36,487.334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "100", | |||||
blur_expected: "100.000", | |||||
focus_expected: "100" | |||||
} | |||||
] | |||||
} | |||||
]; | |||||
} | |||||
}); |
@@ -9,17 +9,20 @@ context('Dashboard links', () => { | |||||
cy.clear_filters(); | cy.clear_filters(); | ||||
cy.visit('/app/user'); | cy.visit('/app/user'); | ||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click(); | |||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true }); | |||||
//To check if initially the dashboard contains only the "Contact" link and there is no counter | //To check if initially the dashboard contains only the "Contact" link and there is no counter | ||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); | cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); | ||||
//Adding a new contact | //Adding a new contact | ||||
cy.get('.btn[data-doctype="Contact"]').click(); | |||||
cy.get('.document-link-badge[data-doctype="Contact"]').click(); | |||||
cy.wait(300); | |||||
cy.findByRole('button', {name: 'Add Contact'}).should('be.visible'); | |||||
cy.findByRole('button', {name: 'Add Contact'}).click(); | |||||
cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin'); | cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin'); | ||||
cy.findByRole('button', {name: 'Save'}).click(); | cy.findByRole('button', {name: 'Save'}).click(); | ||||
cy.visit('/app/user'); | cy.visit('/app/user'); | ||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click(); | |||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true }); | |||||
//To check if the counter for contact doc is "1" after adding the contact | //To check if the counter for contact doc is "1" after adding the contact | ||||
cy.get('[data-doctype="Contact"] > .count').should('contain', '1'); | cy.get('[data-doctype="Contact"] > .count').should('contain', '1'); | ||||
@@ -27,7 +30,7 @@ context('Dashboard links', () => { | |||||
//Deleting the newly created contact | //Deleting the newly created contact | ||||
cy.visit('/app/contact'); | cy.visit('/app/contact'); | ||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click(); | |||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click({ force: true }); | |||||
cy.findByRole('button', {name: 'Actions'}).click(); | cy.findByRole('button', {name: 'Actions'}).click(); | ||||
cy.get('.actions-btn-group [data-label="Delete"]').click(); | cy.get('.actions-btn-group [data-label="Delete"]').click(); | ||||
cy.findByRole('button', {name: 'Yes'}).click({delay: 700}); | cy.findByRole('button', {name: 'Yes'}).click({delay: 700}); | ||||
@@ -36,7 +39,7 @@ context('Dashboard links', () => { | |||||
//To check if the counter from the "Contact" doc link is removed | //To check if the counter from the "Contact" doc link is removed | ||||
cy.wait(700); | cy.wait(700); | ||||
cy.visit('/app/user'); | cy.visit('/app/user'); | ||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click(); | |||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true }); | |||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); | cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); | ||||
}); | }); | ||||
@@ -71,7 +71,7 @@ context('Folder Navigation', () => { | |||||
it('Deleting Test Folder from the home', () => { | it('Deleting Test Folder from the home', () => { | ||||
//Deleting the Test Folder added in the home directory | //Deleting the Test Folder added in the home directory | ||||
cy.visit('/app/file/view/home'); | cy.visit('/app/file/view/home'); | ||||
cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500}); | |||||
cy.get('.level-left > .list-subject > .file-select >.list-row-checkbox').eq(0).click({force: true, delay: 500}); | |||||
cy.findByRole('button', {name: 'Actions'}).click(); | cy.findByRole('button', {name: 'Actions'}).click(); | ||||
cy.get('.actions-btn-group [data-label="Delete"]').click(); | cy.get('.actions-btn-group [data-label="Delete"]').click(); | ||||
cy.findByRole('button', {name: 'Yes'}).click(); | cy.findByRole('button', {name: 'Yes'}).click(); | ||||
@@ -8,7 +8,10 @@ context('Form', () => { | |||||
}); | }); | ||||
it('create a new form', () => { | it('create a new form', () => { | ||||
cy.visit('/app/todo/new'); | cy.visit('/app/todo/new'); | ||||
cy.fill_field('description', 'this is a test todo', 'Text Editor'); | |||||
cy.get('[data-fieldname="description"] .ql-editor') | |||||
.first() | |||||
.click() | |||||
.type('this is a test todo'); | |||||
cy.wait(300); | cy.wait(300); | ||||
cy.get('.page-title').should('contain', 'Not Saved'); | cy.get('.page-title').should('contain', 'Not Saved'); | ||||
cy.intercept({ | cy.intercept({ | ||||
@@ -0,0 +1,31 @@ | |||||
import doctype_with_tab_break from '../fixtures/doctype_with_tab_break'; | |||||
const doctype_name = doctype_with_tab_break.name; | |||||
context("Form Tab Break", () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit('/app/website'); | |||||
return cy.insert_doc('DocType', doctype_with_tab_break, true); | |||||
}); | |||||
it("Should switch tab and open correct tabs on validation error", () => { | |||||
cy.new_form(doctype_name); | |||||
// test tab switch | |||||
cy.findByRole("tab", {name: "Tab 2"}).click(); | |||||
cy.findByText("Phone"); | |||||
cy.findByRole("tab", {name: "Details"}).click(); | |||||
cy.findByText("Name"); | |||||
// form should switch to the tab with un-filled mandatory field | |||||
cy.fill_field("username", "Test"); | |||||
cy.findByRole("button", {name: "Save"}).click(); | |||||
cy.findByText("Missing Fields"); | |||||
cy.hide_dialog(); | |||||
cy.findByText("Phone"); | |||||
cy.fill_field("phone", "12345678"); | |||||
cy.findByRole("button", {name: "Save"}).click(); | |||||
// After save, first tab should have dashboard | |||||
cy.get(".form-tabs > .nav-item").eq(0).click(); | |||||
cy.findByText("Connections"); | |||||
}); | |||||
}); |
@@ -6,6 +6,23 @@ context('List View', () => { | |||||
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); | return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); | ||||
}); | }); | ||||
}); | }); | ||||
it('Keep checkbox checked after Bulk Update', () => { | |||||
cy.go_to_list('ToDo'); | |||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true }); | |||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||||
cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click(); | |||||
cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Due Date').wait(200); | |||||
cy.fill_field('value', '09-28-21', 'Date'); | |||||
cy.get('.modal-footer .standard-actions .btn-primary').click(); | |||||
cy.wait(500); | |||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible'); | |||||
}); | |||||
it('enables "Actions" button', () => { | it('enables "Actions" button', () => { | ||||
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | ||||
cy.go_to_list('ToDo'); | cy.go_to_list('ToDo'); | ||||
@@ -24,10 +41,11 @@ context('List View', () => { | |||||
}).as('real-time-update'); | }).as('real-time-update'); | ||||
cy.wrap(elements).contains('Approve').click(); | cy.wrap(elements).contains('Approve').click(); | ||||
cy.wait(['@bulk-approval', '@real-time-update']); | cy.wait(['@bulk-approval', '@real-time-update']); | ||||
cy.hide_dialog(); | |||||
cy.wait(300); | |||||
cy.get_open_dialog().find('.btn-modal-close').click(); | |||||
cy.reload(); | |||||
cy.clear_filters(); | cy.clear_filters(); | ||||
cy.get('.list-row-container:visible').should('contain', 'Approved'); | cy.get('.list-row-container:visible').should('contain', 'Approved'); | ||||
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
@@ -0,0 +1,58 @@ | |||||
context('MultiSelectDialog', () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit('/app'); | |||||
}); | |||||
function open_multi_select_dialog() { | |||||
cy.window().its('frappe').then(frappe => { | |||||
new frappe.ui.form.MultiSelectDialog({ | |||||
doctype: "Assignment Rule", | |||||
target: {}, | |||||
setters: { | |||||
document_type: null, | |||||
priority: null | |||||
}, | |||||
add_filters_group: 1, | |||||
allow_child_item_selection: 1, | |||||
child_fieldname: "assignment_days", | |||||
child_columns: ["day"] | |||||
}); | |||||
}); | |||||
} | |||||
it('multi select dialog api works', () => { | |||||
open_multi_select_dialog(); | |||||
cy.get_open_dialog().should('contain', 'Select Assignment Rules'); | |||||
}); | |||||
it('checks for filters', () => { | |||||
['search_term', 'document_type', 'priority'].forEach(fieldname => { | |||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist'); | |||||
}); | |||||
// add_filters_group: 1 should add a filter group | |||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist'); | |||||
}); | |||||
it('checks for child item selection', () => { | |||||
cy.get_open_dialog() | |||||
.get(`.dt-row-header`).should('not.exist'); | |||||
cy.get_open_dialog() | |||||
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`) | |||||
.should('exist') | |||||
.click(); | |||||
cy.get_open_dialog() | |||||
.get(`.frappe-control[data-fieldname="child_selection_area"]`) | |||||
.should('exist'); | |||||
cy.get_open_dialog() | |||||
.get(`.dt-row-header`).should('contain', 'Assignment Rule'); | |||||
cy.get_open_dialog() | |||||
.get(`.dt-row-header`).should('contain', 'Day'); | |||||
}); | |||||
}); |
@@ -1,7 +1,6 @@ | |||||
context('Navigation', () => { | context('Navigation', () => { | ||||
before(() => { | before(() => { | ||||
cy.login(); | cy.login(); | ||||
cy.visit('/app/website'); | |||||
}); | }); | ||||
it('Navigate to route with hash in document name', () => { | it('Navigate to route with hash in document name', () => { | ||||
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true}); | cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true}); | ||||
@@ -11,4 +10,16 @@ context('Navigation', () => { | |||||
cy.go('back'); | cy.go('back'); | ||||
cy.title().should('eq', 'Website'); | cy.title().should('eq', 'Website'); | ||||
}); | }); | ||||
it.only('Navigate to previous page after login', () => { | |||||
cy.visit('/app/todo'); | |||||
cy.findByTitle('To Do').should('be.visible'); | |||||
cy.request('/api/method/logout'); | |||||
cy.reload(); | |||||
cy.get('.btn-primary').contains('Login').click(); | |||||
cy.location('pathname').should('eq', '/login'); | |||||
cy.login(); | |||||
cy.visit('/app'); | |||||
cy.location('pathname').should('eq', '/app/todo'); | |||||
}); | |||||
}); | }); |
@@ -11,6 +11,7 @@ context('Timeline', () => { | |||||
cy.visit('/app/todo'); | cy.visit('/app/todo'); | ||||
cy.click_listview_primary_button('Add ToDo'); | cy.click_listview_primary_button('Add ToDo'); | ||||
cy.findByRole('button', {name: 'Edit in full page'}).click(); | cy.findByRole('button', {name: 'Edit in full page'}).click(); | ||||
cy.findByTitle('New ToDo').should('be.visible'); | |||||
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); | cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); | ||||
cy.wait(200); | cy.wait(200); | ||||
cy.findByRole('button', {name: 'Save'}).click(); | cy.findByRole('button', {name: 'Save'}).click(); | ||||
@@ -5,14 +5,16 @@ context('Timeline Email', () => { | |||||
cy.visit('/app/todo'); | cy.visit('/app/todo'); | ||||
}); | }); | ||||
it('Adding new ToDo, adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { | |||||
//Adding new ToDo | |||||
it('Adding new ToDo', () => { | |||||
cy.click_listview_primary_button('Add ToDo'); | cy.click_listview_primary_button('Add ToDo'); | ||||
cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500}); | cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500}); | ||||
cy.fill_field("description", "Test ToDo", "Text Editor"); | cy.fill_field("description", "Test ToDo", "Text Editor"); | ||||
cy.wait(500); | cy.wait(500); | ||||
cy.get('.primary-action').contains('Save').click({force: true}); | cy.get('.primary-action').contains('Save').click({force: true}); | ||||
cy.wait(700); | cy.wait(700); | ||||
}); | |||||
it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { | |||||
cy.visit('/app/todo'); | cy.visit('/app/todo'); | ||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click(); | cy.get('.list-row > .level-left > .list-subject').eq(0).click(); | ||||
@@ -41,11 +43,13 @@ context('Timeline Email', () => { | |||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click(); | cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click(); | ||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click(); | cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click(); | ||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click(); | cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click(); | ||||
cy.visit('/app/todo'); | cy.visit('/app/todo'); | ||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); | cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); | ||||
//Removing the added attachment | //Removing the added attachment | ||||
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click(); | cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click(); | ||||
cy.wait(500); | |||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click(); | cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click(); | ||||
//To check if the removed attachment is shown in the timeline content | //To check if the removed attachment is shown in the timeline content | ||||
@@ -235,12 +235,13 @@ def connect_replica(): | |||||
from frappe.database import get_db | from frappe.database import get_db | ||||
user = local.conf.db_name | user = local.conf.db_name | ||||
password = local.conf.db_password | password = local.conf.db_password | ||||
port = local.conf.replica_db_port | |||||
if local.conf.different_credentials_for_replica: | if local.conf.different_credentials_for_replica: | ||||
user = local.conf.replica_db_name | user = local.conf.replica_db_name | ||||
password = local.conf.replica_db_password | password = local.conf.replica_db_password | ||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password) | |||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port) | |||||
# swap db connections | # swap db connections | ||||
local.primary_db = local.db | local.primary_db = local.db | ||||
@@ -1,10 +1,11 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import os | import os | ||||
import re | import re | ||||
import json | import json | ||||
import shutil | import shutil | ||||
import subprocess | import subprocess | ||||
from subprocess import getoutput | |||||
from io import StringIO | from io import StringIO | ||||
from tempfile import mkdtemp, mktemp | from tempfile import mkdtemp, mktemp | ||||
from distutils.spawn import find_executable | from distutils.spawn import find_executable | ||||
@@ -17,6 +18,8 @@ import psutil | |||||
from urllib.parse import urlparse | from urllib.parse import urlparse | ||||
from simple_chalk import green | from simple_chalk import green | ||||
from semantic_version import Version | from semantic_version import Version | ||||
from requests import head | |||||
from requests.exceptions import HTTPError | |||||
timestamps = {} | timestamps = {} | ||||
@@ -24,6 +27,12 @@ app_paths = None | |||||
sites_path = os.path.abspath(os.getcwd()) | sites_path = os.path.abspath(os.getcwd()) | ||||
class AssetsNotDownloadedError(Exception): | |||||
pass | |||||
class AssetsDontExistError(HTTPError): | |||||
pass | |||||
def download_file(url, prefix): | def download_file(url, prefix): | ||||
from requests import get | from requests import get | ||||
@@ -70,81 +79,94 @@ def build_missing_files(): | |||||
bundle(build_mode, apps="frappe") | bundle(build_mode, apps="frappe") | ||||
def get_assets_link(frappe_head): | |||||
from subprocess import getoutput | |||||
from requests import head | |||||
def get_assets_link(frappe_head) -> str: | |||||
tag = getoutput( | tag = getoutput( | ||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" | |||||
r" refs/tags/,,' -e 's/\^{}//'" | |||||
% frappe_head | |||||
) | |||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" | |||||
r" refs/tags/,,' -e 's/\^{}//'" | |||||
% frappe_head | |||||
) | |||||
if tag: | if tag: | ||||
# if tag exists, download assets from github release | # if tag exists, download assets from github release | ||||
url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag) | |||||
url = f"https://github.com/frappe/frappe/releases/download/{tag}/assets.tar.gz" | |||||
else: | else: | ||||
url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head) | |||||
url = f"http://assets.frappeframework.com/{frappe_head}.tar.gz" | |||||
if not head(url): | if not head(url): | ||||
raise ValueError("URL {0} doesn't exist".format(url)) | |||||
reference = f"Release {tag}" if tag else f"Commit {frappe_head}" | |||||
raise AssetsDontExistError(f"Assets for {reference} don't exist") | |||||
return url | return url | ||||
def fetch_assets(url, frappe_head): | |||||
click.secho("Retrieving assets...", fg="yellow") | |||||
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head) | |||||
assets_archive = download_file(url, prefix) | |||||
if not assets_archive: | |||||
raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}") | |||||
print(f"\n{green('✔')} Downloaded Frappe assets from {url}") | |||||
return assets_archive | |||||
def setup_assets(assets_archive): | |||||
import tarfile | |||||
directories_created = set() | |||||
click.secho("\nExtracting assets...\n", fg="yellow") | |||||
with tarfile.open(assets_archive) as tar: | |||||
for file in tar: | |||||
if not file.isdir(): | |||||
dest = "." + file.name.replace("./frappe-bench/sites", "") | |||||
asset_directory = os.path.dirname(dest) | |||||
show = dest.replace("./assets/", "") | |||||
if asset_directory not in directories_created: | |||||
if not os.path.exists(asset_directory): | |||||
os.makedirs(asset_directory, exist_ok=True) | |||||
directories_created.add(asset_directory) | |||||
tar.makefile(file, dest) | |||||
print("{0} Restored {1}".format(green('✔'), show)) | |||||
return directories_created | |||||
def download_frappe_assets(verbose=True): | def download_frappe_assets(verbose=True): | ||||
"""Downloads and sets up Frappe assets if they exist based on the current | """Downloads and sets up Frappe assets if they exist based on the current | ||||
commit HEAD. | commit HEAD. | ||||
Returns True if correctly setup else returns False. | Returns True if correctly setup else returns False. | ||||
""" | """ | ||||
from subprocess import getoutput | |||||
assets_setup = False | |||||
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD") | frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD") | ||||
if frappe_head: | |||||
if not frappe_head: | |||||
return False | |||||
try: | |||||
url = get_assets_link(frappe_head) | |||||
assets_archive = fetch_assets(url, frappe_head) | |||||
setup_assets(assets_archive) | |||||
build_missing_files() | |||||
return True | |||||
except AssetsDontExistError as e: | |||||
click.secho(str(e), fg="yellow") | |||||
except Exception as e: | |||||
# TODO: log traceback in bench.log | |||||
click.secho(str(e), fg="red") | |||||
finally: | |||||
try: | try: | ||||
url = get_assets_link(frappe_head) | |||||
click.secho("Retrieving assets...", fg="yellow") | |||||
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head) | |||||
assets_archive = download_file(url, prefix) | |||||
print("\n{0} Downloaded Frappe assets from {1}".format(green('✔'), url)) | |||||
if assets_archive: | |||||
import tarfile | |||||
directories_created = set() | |||||
click.secho("\nExtracting assets...\n", fg="yellow") | |||||
with tarfile.open(assets_archive) as tar: | |||||
for file in tar: | |||||
if not file.isdir(): | |||||
dest = "." + file.name.replace("./frappe-bench/sites", "") | |||||
asset_directory = os.path.dirname(dest) | |||||
show = dest.replace("./assets/", "") | |||||
if asset_directory not in directories_created: | |||||
if not os.path.exists(asset_directory): | |||||
os.makedirs(asset_directory, exist_ok=True) | |||||
directories_created.add(asset_directory) | |||||
tar.makefile(file, dest) | |||||
print("{0} Restored {1}".format(green('✔'), show)) | |||||
build_missing_files() | |||||
return True | |||||
else: | |||||
raise | |||||
shutil.rmtree(os.path.dirname(assets_archive)) | |||||
except Exception: | except Exception: | ||||
# TODO: log traceback in bench.log | |||||
click.secho("An Error occurred while downloading assets...", fg="red") | |||||
assets_setup = False | |||||
finally: | |||||
try: | |||||
shutil.rmtree(os.path.dirname(assets_archive)) | |||||
except Exception: | |||||
pass | |||||
return assets_setup | |||||
pass | |||||
return False | |||||
def symlink(target, link_name, overwrite=False): | def symlink(target, link_name, overwrite=False): | ||||
@@ -102,9 +102,24 @@ def get_commands(): | |||||
from .site import commands as site_commands | from .site import commands as site_commands | ||||
from .translate import commands as translate_commands | from .translate import commands as translate_commands | ||||
from .utils import commands as utils_commands | from .utils import commands as utils_commands | ||||
from .redis import commands as redis_commands | |||||
from .redis_utils import commands as redis_commands | |||||
clickable_link = ( | |||||
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a" | |||||
) | |||||
all_commands = ( | |||||
scheduler_commands | |||||
+ site_commands | |||||
+ translate_commands | |||||
+ utils_commands | |||||
+ redis_commands | |||||
) | |||||
for command in all_commands: | |||||
if not command.help: | |||||
command.help = f"Refer to {clickable_link}" | |||||
return all_commands | |||||
all_commands = scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands | |||||
return list(set(all_commands)) | |||||
commands = get_commands() | commands = get_commands() |
@@ -3,7 +3,7 @@ import os | |||||
import click | import click | ||||
import frappe | import frappe | ||||
from frappe.utils.rq import RedisQueue | |||||
from frappe.utils.redis_queue import RedisQueue | |||||
from frappe.installer import update_site_config | from frappe.installer import update_site_config | ||||
@click.command('create-rq-users') | @click.command('create-rq-users') |
@@ -67,6 +67,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas | |||||
validate_database_sql | validate_database_sql | ||||
) | ) | ||||
site = get_site(context) | |||||
frappe.init(site=site) | |||||
force = context.force or force | force = context.force or force | ||||
decompressed_file_name = extract_sql_from_archive(sql_file_path) | decompressed_file_name = extract_sql_from_archive(sql_file_path) | ||||
@@ -85,9 +88,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas | |||||
# check if valid SQL file | # check if valid SQL file | ||||
validate_database_sql(decompressed_file_name, _raise=not force) | validate_database_sql(decompressed_file_name, _raise=not force) | ||||
site = get_site(context) | |||||
frappe.init(site=site) | |||||
# dont allow downgrading to older versions of frappe without force | # dont allow downgrading to older versions of frappe without force | ||||
if not force and is_downgrade(decompressed_file_name, verbose=True): | if not force and is_downgrade(decompressed_file_name, verbose=True): | ||||
warn_message = ( | warn_message = ( | ||||
@@ -474,7 +474,7 @@ def remove_from_installed_apps(context, app): | |||||
@click.command('uninstall-app') | @click.command('uninstall-app') | ||||
@click.argument('app') | @click.argument('app') | ||||
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True) | |||||
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False) | |||||
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) | @click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) | ||||
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False) | @click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False) | ||||
@click.option('--force', help='Force remove app from site', is_flag=True, default=False) | @click.option('--force', help='Force remove app from site', is_flag=True, default=False) | ||||
@@ -738,6 +738,131 @@ def build_search_index(context): | |||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
@click.command('trim-database') | |||||
@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted') | |||||
@click.option('--format', '-f', default='text', type=click.Choice(['json', 'text']), help='Output format') | |||||
@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site') | |||||
@pass_context | |||||
def trim_database(context, dry_run, format, no_backup): | |||||
if not context.sites: | |||||
raise SiteNotSpecifiedError | |||||
from frappe.utils.backups import scheduled_backup | |||||
ALL_DATA = {} | |||||
for site in context.sites: | |||||
frappe.init(site=site) | |||||
frappe.connect() | |||||
TABLES_TO_DROP = [] | |||||
STANDARD_TABLES = get_standard_tables() | |||||
information_schema = frappe.qb.Schema("information_schema") | |||||
table_name = frappe.qb.Field("table_name").as_("name") | |||||
queried_result = frappe.qb.from_( | |||||
information_schema.tables | |||||
).select(table_name).where( | |||||
information_schema.tables.table_schema == frappe.conf.db_name | |||||
).run() | |||||
database_tables = [x[0] for x in queried_result] | |||||
doctype_tables = frappe.get_all("DocType", pluck="name") | |||||
for x in database_tables: | |||||
doctype = x.lstrip("tab") | |||||
if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES): | |||||
TABLES_TO_DROP.append(x) | |||||
if not TABLES_TO_DROP: | |||||
if format == "text": | |||||
click.secho(f"No ghost tables found in {frappe.local.site}...Great!", fg="green") | |||||
else: | |||||
if not (no_backup or dry_run): | |||||
if format == "text": | |||||
print(f"Backing Up Tables: {', '.join(TABLES_TO_DROP)}") | |||||
odb = scheduled_backup( | |||||
ignore_conf=False, | |||||
include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP), | |||||
ignore_files=True, | |||||
force=True, | |||||
) | |||||
if format == "text": | |||||
odb.print_summary() | |||||
print("\nTrimming Database") | |||||
for table in TABLES_TO_DROP: | |||||
if format == "text": | |||||
print(f"* Dropping Table '{table}'...") | |||||
if not dry_run: | |||||
frappe.db.sql_ddl(f"drop table `{table}`") | |||||
ALL_DATA[frappe.local.site] = TABLES_TO_DROP | |||||
frappe.destroy() | |||||
if format == "json": | |||||
import json | |||||
print(json.dumps(ALL_DATA, indent=1)) | |||||
def get_standard_tables(): | |||||
import re | |||||
tables = [] | |||||
sql_file = os.path.join( | |||||
"..", "apps", "frappe", "frappe", "database", frappe.conf.db_type, f'framework_{frappe.conf.db_type}.sql' | |||||
) | |||||
content = open(sql_file).read().splitlines() | |||||
for line in content: | |||||
table_found = re.search(r"""CREATE TABLE ("|`)(.*)?("|`) \(""", line) | |||||
if table_found: | |||||
tables.append(table_found.group(2)) | |||||
return tables | |||||
@click.command('trim-tables') | |||||
@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted') | |||||
@click.option('--format', '-f', default='table', type=click.Choice(['json', 'table']), help='Output format') | |||||
@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site') | |||||
@pass_context | |||||
def trim_tables(context, dry_run, format, no_backup): | |||||
if not context.sites: | |||||
raise SiteNotSpecifiedError | |||||
from frappe.model.meta import trim_tables | |||||
from frappe.utils.backups import scheduled_backup | |||||
for site in context.sites: | |||||
frappe.init(site=site) | |||||
frappe.connect() | |||||
if not (no_backup or dry_run): | |||||
click.secho(f"Taking backup for {frappe.local.site}", fg="green") | |||||
odb = scheduled_backup(ignore_files=False, force=True) | |||||
odb.print_summary() | |||||
try: | |||||
trimmed_data = trim_tables(dry_run=dry_run, quiet=format == 'json') | |||||
if format == 'table' and not dry_run: | |||||
click.secho(f"The following data have been removed from {frappe.local.site}", fg='green') | |||||
handle_data(trimmed_data, format=format) | |||||
finally: | |||||
frappe.destroy() | |||||
def handle_data(data: dict, format='json'): | |||||
if format == 'json': | |||||
import json | |||||
print(json.dumps({frappe.local.site: data}, indent=1, sort_keys=True)) | |||||
else: | |||||
from frappe.utils.commands import render_table | |||||
data = [["DocType", "Fields"]] + [[table, ", ".join(columns)] for table, columns in data.items()] | |||||
render_table(data) | |||||
commands = [ | commands = [ | ||||
add_system_manager, | add_system_manager, | ||||
backup, | backup, | ||||
@@ -766,5 +891,7 @@ commands = [ | |||||
add_to_hosts, | add_to_hosts, | ||||
start_ngrok, | start_ngrok, | ||||
build_search_index, | build_search_index, | ||||
partial_restore | |||||
partial_restore, | |||||
trim_tables, | |||||
trim_database, | |||||
] | ] |
@@ -408,20 +408,47 @@ def bulk_rename(context, doctype, path): | |||||
frappe.destroy() | frappe.destroy() | ||||
@click.command('db-console') | |||||
@pass_context | |||||
def database(context): | |||||
""" | |||||
Enter into the Database console for given site. | |||||
""" | |||||
site = get_site(context) | |||||
if not site: | |||||
raise SiteNotSpecifiedError | |||||
frappe.init(site=site) | |||||
if not frappe.conf.db_type or frappe.conf.db_type == "mariadb": | |||||
_mariadb() | |||||
elif frappe.conf.db_type == "postgres": | |||||
_psql() | |||||
@click.command('mariadb') | @click.command('mariadb') | ||||
@pass_context | @pass_context | ||||
def mariadb(context): | def mariadb(context): | ||||
""" | """ | ||||
Enter into mariadb console for a given site. | Enter into mariadb console for a given site. | ||||
""" | """ | ||||
import os | |||||
site = get_site(context) | site = get_site(context) | ||||
if not site: | if not site: | ||||
raise SiteNotSpecifiedError | raise SiteNotSpecifiedError | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
_mariadb() | |||||
@click.command('postgres') | |||||
@pass_context | |||||
def postgres(context): | |||||
""" | |||||
Enter into postgres console for a given site. | |||||
""" | |||||
site = get_site(context) | |||||
frappe.init(site=site) | |||||
_psql() | |||||
# This is assuming you're within the bench instance. | |||||
def _mariadb(): | |||||
mysql = find_executable('mysql') | mysql = find_executable('mysql') | ||||
os.execv(mysql, [ | os.execv(mysql, [ | ||||
mysql, | mysql, | ||||
@@ -434,15 +461,7 @@ def mariadb(context): | |||||
"-A"]) | "-A"]) | ||||
@click.command('postgres') | |||||
@pass_context | |||||
def postgres(context): | |||||
""" | |||||
Enter into postgres console for a given site. | |||||
""" | |||||
site = get_site(context) | |||||
frappe.init(site=site) | |||||
# This is assuming you're within the bench instance. | |||||
def _psql(): | |||||
psql = find_executable('psql') | psql = find_executable('psql') | ||||
subprocess.run([ psql, '-d', frappe.conf.db_name]) | subprocess.run([ psql, '-d', frappe.conf.db_name]) | ||||
@@ -525,6 +544,74 @@ def console(context, autoreload=False): | |||||
terminal() | terminal() | ||||
@click.command('transform-database', help="Change tables' internal settings changing engine and row formats") | |||||
@click.option('--table', required=True, help="Comma separated name of tables to convert. To convert all tables, pass 'all'") | |||||
@click.option('--engine', default=None, type=click.Choice(["InnoDB", "MyISAM"]), help="Choice of storage engine for said table(s)") | |||||
@click.option('--row_format', default=None, type=click.Choice(["DYNAMIC", "COMPACT", "REDUNDANT", "COMPRESSED"]), help="Set ROW_FORMAT parameter for said table(s)") | |||||
@click.option('--failfast', is_flag=True, default=False, help="Exit on first failure occurred") | |||||
@pass_context | |||||
def transform_database(context, table, engine, row_format, failfast): | |||||
"Transform site database through given parameters" | |||||
site = get_site(context) | |||||
check_table = [] | |||||
add_line = False | |||||
skipped = 0 | |||||
frappe.init(site=site) | |||||
if frappe.conf.db_type and frappe.conf.db_type != "mariadb": | |||||
click.secho("This command only has support for MariaDB databases at this point", fg="yellow") | |||||
sys.exit(1) | |||||
if not (engine or row_format): | |||||
click.secho("Values for `--engine` or `--row_format` must be set") | |||||
sys.exit(1) | |||||
frappe.connect() | |||||
if table == "all": | |||||
information_schema = frappe.qb.Schema("information_schema") | |||||
queried_tables = frappe.qb.from_( | |||||
information_schema.tables | |||||
).select("table_name").where( | |||||
(information_schema.tables.row_format != row_format) | |||||
& (information_schema.tables.table_schema == frappe.conf.db_name) | |||||
).run() | |||||
tables = [x[0] for x in queried_tables] | |||||
else: | |||||
tables = [x.strip() for x in table.split(",")] | |||||
total = len(tables) | |||||
for current, table in enumerate(tables): | |||||
values_to_set = "" | |||||
if engine: | |||||
values_to_set += f" ENGINE={engine}" | |||||
if row_format: | |||||
values_to_set += f" ROW_FORMAT={row_format}" | |||||
try: | |||||
frappe.db.sql(f"ALTER TABLE `{table}`{values_to_set}") | |||||
update_progress_bar("Updating table schema", current - skipped, total) | |||||
add_line = True | |||||
except Exception as e: | |||||
check_table.append([table, e.args]) | |||||
skipped += 1 | |||||
if failfast: | |||||
break | |||||
if add_line: | |||||
print() | |||||
for errored_table in check_table: | |||||
table, err = errored_table | |||||
err_msg = f"{table}: ERROR {err[0]}: {err[1]}" | |||||
click.secho(err_msg, fg="yellow") | |||||
frappe.destroy() | |||||
@click.command('run-tests') | @click.command('run-tests') | ||||
@click.option('--app', help="For App") | @click.option('--app', help="For App") | ||||
@click.option('--doctype', help="For DocType") | @click.option('--doctype', help="For DocType") | ||||
@@ -814,6 +901,8 @@ commands = [ | |||||
build, | build, | ||||
clear_cache, | clear_cache, | ||||
clear_website_cache, | clear_website_cache, | ||||
database, | |||||
transform_database, | |||||
jupyter, | jupyter, | ||||
console, | console, | ||||
destroy_all_sessions, | destroy_all_sessions, | ||||
@@ -178,4 +178,4 @@ def set_link_title(doc): | |||||
for link in doc.links: | for link in doc.links: | ||||
if not link.link_title: | if not link.link_title: | ||||
linked_doc = frappe.get_doc(link.link_doctype, link.link_name) | linked_doc = frappe.get_doc(link.link_doctype, link.link_name) | ||||
link.link_title = linked_doc.get("title_field") or linked_doc.get("name") | |||||
link.link_title = linked_doc.get_title() or link.link_name |
@@ -65,7 +65,7 @@ class Address(Document): | |||||
def has_link(self, doctype, name): | def has_link(self, doctype, name): | ||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype==doctype and link.link_name== name: | |||||
if link.link_doctype == doctype and link.link_name == name: | |||||
return True | return True | ||||
def has_common_link(self, doc): | def has_common_link(self, doc): | ||||
@@ -47,14 +47,14 @@ class Contact(Document): | |||||
def get_link_for(self, link_doctype): | def get_link_for(self, link_doctype): | ||||
'''Return the link name, if exists for the given link DocType''' | '''Return the link name, if exists for the given link DocType''' | ||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype==link_doctype: | |||||
if link.link_doctype == link_doctype: | |||||
return link.link_name | return link.link_name | ||||
return None | return None | ||||
def has_link(self, doctype, name): | def has_link(self, doctype, name): | ||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype==doctype and link.link_name== name: | |||||
if link.link_doctype == doctype and link.link_name == name: | |||||
return True | return True | ||||
def has_common_link(self, doc): | def has_common_link(self, doc): | ||||
@@ -1,6 +1,7 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and contributors | |||||
# Copyright (c) 2021, Frappe Technologies and contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
@@ -10,25 +11,40 @@ class AccessLog(Document): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.write_only() | @frappe.write_only() | ||||
def make_access_log(doctype=None, document=None, method=None, file_type=None, | |||||
report_name=None, filters=None, page=None, columns=None): | |||||
@retry( | |||||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError) | |||||
) | |||||
def make_access_log( | |||||
doctype=None, | |||||
document=None, | |||||
method=None, | |||||
file_type=None, | |||||
report_name=None, | |||||
filters=None, | |||||
page=None, | |||||
columns=None, | |||||
): | |||||
user = frappe.session.user | user = frappe.session.user | ||||
in_request = frappe.request and frappe.request.method == "GET" | |||||
doc = frappe.get_doc({ | |||||
'doctype': 'Access Log', | |||||
'user': user, | |||||
'export_from': doctype, | |||||
'reference_document': document, | |||||
'file_type': file_type, | |||||
'report_name': report_name, | |||||
'page': page, | |||||
'method': method, | |||||
'filters': frappe.utils.cstr(filters) if filters else None, | |||||
'columns': columns | |||||
}) | |||||
doc = frappe.get_doc( | |||||
{ | |||||
"doctype": "Access Log", | |||||
"user": user, | |||||
"export_from": doctype, | |||||
"reference_document": document, | |||||
"file_type": file_type, | |||||
"report_name": report_name, | |||||
"page": page, | |||||
"method": method, | |||||
"filters": frappe.utils.cstr(filters) if filters else None, | |||||
"columns": columns, | |||||
} | |||||
) | |||||
doc.insert(ignore_permissions=True) | doc.insert(ignore_permissions=True) | ||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | ||||
if frappe.request and frappe.request.method == 'GET': | |||||
# dont commit in test mode | |||||
if not frappe.flags.in_test or in_request: | |||||
frappe.db.commit() | frappe.db.commit() |
@@ -274,6 +274,8 @@ class DocType(Document): | |||||
d.fieldname = d.fieldname + '_section' | d.fieldname = d.fieldname + '_section' | ||||
elif d.fieldtype=='Column Break': | elif d.fieldtype=='Column Break': | ||||
d.fieldname = d.fieldname + '_column' | d.fieldname = d.fieldname + '_column' | ||||
elif d.fieldtype=='Tab Break': | |||||
d.fieldname = d.fieldname + '_tab' | |||||
else: | else: | ||||
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) | d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) | ||||
else: | else: | ||||
@@ -41,6 +41,7 @@ | |||||
"fieldname": "counter", | "fieldname": "counter", | ||||
"fieldtype": "Int", | "fieldtype": "Int", | ||||
"label": "Counter", | "label": "Counter", | ||||
"no_copy": 1, | |||||
"read_only": 1 | "read_only": 1 | ||||
}, | }, | ||||
{ | { | ||||
@@ -79,7 +80,7 @@ | |||||
], | ], | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-11-04 14:38:14.836056", | |||||
"modified": "2021-09-13 20:07:47.617615", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Document Naming Rule", | "name": "Document Naming Rule", | ||||
@@ -1,238 +1,80 @@ | |||||
{ | { | ||||
"allow_copy": 1, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"beta": 0, | |||||
"creation": "2013-01-10 16:34:24", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"editable_grid": 0, | |||||
"actions": [], | |||||
"allow_copy": 1, | |||||
"creation": "2013-01-10 16:34:24", | |||||
"doctype": "DocType", | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"sms_gateway_url", | |||||
"message_parameter", | |||||
"receiver_parameter", | |||||
"static_parameters_section", | |||||
"parameters", | |||||
"use_post" | |||||
], | |||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Eg. smsgateway.com/api/send_sms.cgi", | |||||
"fieldname": "sms_gateway_url", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "SMS Gateway URL", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
"description": "Eg. smsgateway.com/api/send_sms.cgi", | |||||
"fieldname": "sms_gateway_url", | |||||
"fieldtype": "Small Text", | |||||
"in_list_view": 1, | |||||
"label": "SMS Gateway URL", | |||||
"reqd": 1 | |||||
}, | |||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Enter url parameter for message", | |||||
"fieldname": "message_parameter", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Message Parameter", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
"description": "Enter url parameter for message", | |||||
"fieldname": "message_parameter", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Message Parameter", | |||||
"reqd": 1 | |||||
}, | |||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Enter url parameter for receiver nos", | |||||
"fieldname": "receiver_parameter", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Receiver Parameter", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
"description": "Enter url parameter for receiver nos", | |||||
"fieldname": "receiver_parameter", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Receiver Parameter", | |||||
"reqd": 1 | |||||
}, | |||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "static_parameters_section", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0, | |||||
"fieldname": "static_parameters_section", | |||||
"fieldtype": "Column Break", | |||||
"width": "50%" | "width": "50%" | ||||
}, | |||||
}, | |||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)", | |||||
"fieldname": "parameters", | |||||
"fieldtype": "Table", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Static Parameters", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "SMS Parameter", | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
"description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)", | |||||
"fieldname": "parameters", | |||||
"fieldtype": "Table", | |||||
"label": "Static Parameters", | |||||
"options": "SMS Parameter" | |||||
}, | |||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "use_post", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Use POST", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
"default": "0", | |||||
"fieldname": "use_post", | |||||
"fieldtype": "Check", | |||||
"label": "Use POST" | |||||
} | } | ||||
], | |||||
"has_web_view": 0, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"icon": "fa fa-cog", | |||||
"idx": 1, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 1, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2021-03-02 18:06:00.868688", | |||||
"modified_by": "Administrator", | |||||
"module": "Core", | |||||
"name": "SMS Settings", | |||||
"owner": "Administrator", | |||||
], | |||||
"icon": "fa fa-cog", | |||||
"idx": 1, | |||||
"issingle": 1, | |||||
"links": [], | |||||
"modified": "2021-09-21 19:45:26.809793", | |||||
"modified_by": "Administrator", | |||||
"module": "Core", | |||||
"name": "SMS Settings", | |||||
"owner": "Administrator", | |||||
"permissions": [ | "permissions": [ | ||||
{ | { | ||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 0, | |||||
"email": 0, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 0, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"create": 1, | |||||
"read": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | "write": 1 | ||||
} | } | ||||
], | |||||
"quick_entry": 0, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"show_name_in_global_search": 0, | |||||
"track_changes": 1, | |||||
"track_seen": 0 | |||||
} | |||||
], | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1 | |||||
} |
@@ -788,7 +788,7 @@ def sign_up(email, full_name, redirect_to): | |||||
return 2, _("Please ask your administrator to verify your sign-up") | return 2, _("Please ask your administrator to verify your sign-up") | ||||
@frappe.whitelist(allow_guest=True) | @frappe.whitelist(allow_guest=True) | ||||
@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST']) | |||||
@rate_limit(limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST']) | |||||
def reset_password(user): | def reset_password(user): | ||||
if user=="Administrator": | if user=="Administrator": | ||||
return 'not allowed' | return 'not allowed' | ||||
@@ -1,21 +0,0 @@ | |||||
.version-info { | |||||
overflow: auto; | |||||
} | |||||
.version-info pre { | |||||
border: 0px; | |||||
margin: 0px; | |||||
background-color: inherit; | |||||
} | |||||
.version-info .table { | |||||
background-color: inherit; | |||||
} | |||||
.version-info .success { | |||||
background-color: #dff0d8 !important; | |||||
} | |||||
.version-info .danger { | |||||
background-color: #f2dede !important; | |||||
} |
@@ -1,8 +1,6 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
# License: MIT. See LICENSE | |||||
import frappe, json | import frappe, json | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
@@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]: | |||||
fail_registry = queue.failed_job_registry | fail_registry = queue.failed_job_registry | ||||
for job_id in fail_registry.get_job_ids(): | for job_id in fail_registry.get_job_ids(): | ||||
job = queue.fetch_job(job_id) | job = queue.fetch_job(job_id) | ||||
add_job(job, queue.name) | |||||
if job: | |||||
add_job(job, queue.name) | |||||
return jobs | return jobs | ||||
@@ -1,460 +1,458 @@ | |||||
{ | { | ||||
"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", | |||||
"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:22.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", | |||||
"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 | |||||
} | } |
@@ -18,7 +18,7 @@ class CustomField(Document): | |||||
if not self.fieldname: | if not self.fieldname: | ||||
label = self.label | label = self.label | ||||
if not label: | if not label: | ||||
if self.fieldtype in ["Section Break", "Column Break"]: | |||||
if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]: | |||||
label = self.fieldtype + "_" + str(self.idx) | label = self.fieldtype + "_" + str(self.idx) | ||||
else: | else: | ||||
frappe.throw(_("Label is mandatory")) | frappe.throw(_("Label is mandatory")) | ||||
@@ -82,7 +82,7 @@ | |||||
"label": "Type", | "label": "Type", | ||||
"oldfieldname": "fieldtype", | "oldfieldname": "fieldtype", | ||||
"oldfieldtype": "Select", | "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\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime", | |||||
"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\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break", | |||||
"reqd": 1, | "reqd": 1, | ||||
"search_index": 1 | "search_index": 1 | ||||
}, | }, | ||||
@@ -428,7 +428,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"istable": 1, | "istable": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2021-07-10 21:57:24.479749", | |||||
"modified": "2021-07-11 21:57:24.479749", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Customize Form Field", | "name": "Customize Form Field", | ||||
@@ -34,7 +34,7 @@ class PropertySetter(Document): | |||||
fields=['fieldname', 'label', 'fieldtype'], | fields=['fieldname', 'label', 'fieldtype'], | ||||
filters={ | filters={ | ||||
'parent': dt, | 'parent': dt, | ||||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], | |||||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], | |||||
'fieldname': ['!=', ''] | 'fieldname': ['!=', ''] | ||||
}, | }, | ||||
order_by='label asc', | order_by='label asc', | ||||
@@ -1,45 +0,0 @@ | |||||
{ | |||||
"db_name": "testdb", | |||||
"db_password": "password", | |||||
"mute_emails": true, | |||||
"limits": { | |||||
"emails": 1500, | |||||
"space": 0.157, | |||||
"expiry": "2016-07-25", | |||||
"users": 1 | |||||
}, | |||||
"developer_mode": 1, | |||||
"auto_cache_clear": true, | |||||
"disable_website_cache": true, | |||||
"max_file_size": 1000000, | |||||
"mail_server": "localhost", | |||||
"mail_login": null, | |||||
"mail_password": null, | |||||
"mail_port": 25, | |||||
"use_ssl": 0, | |||||
"auto_email_id": "hello@example.com", | |||||
"google_analytics_id": "google_analytics_id", | |||||
"google_analytics_anonymize_ip": 1, | |||||
"google_login": { | |||||
"client_id": "google_client_id", | |||||
"client_secret": "google_client_secret" | |||||
}, | |||||
"github_login": { | |||||
"client_id": "github_client_id", | |||||
"client_secret": "github_client_secret" | |||||
}, | |||||
"facebook_login": { | |||||
"client_id": "facebook_client_id", | |||||
"client_secret": "facebook_client_secret" | |||||
}, | |||||
"celery_broker": "redis://localhost", | |||||
"celery_result_backend": null, | |||||
"scheduler_interval": 300, | |||||
"celery_queue_per_site": true | |||||
} |
@@ -332,7 +332,7 @@ class Database(object): | |||||
values[key] = value | values[key] = value | ||||
if isinstance(value, (list, tuple)): | if isinstance(value, (list, tuple)): | ||||
# value is a tuple like ("!=", 0) | # value is a tuple like ("!=", 0) | ||||
_operator = value[0] | |||||
_operator = value[0].lower() | |||||
values[key] = value[1] | values[key] = value[1] | ||||
if isinstance(value[1], (tuple, list)): | if isinstance(value[1], (tuple, list)): | ||||
# value is a list in tuple ("in", ("A", "B")) | # value is a list in tuple ("in", ("A", "B")) | ||||
@@ -919,13 +919,13 @@ class Database(object): | |||||
WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0] | WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0] | ||||
def has_index(self, table_name, index_name): | def has_index(self, table_name, index_name): | ||||
pass | |||||
raise NotImplementedError | |||||
def add_index(self, doctype, fields, index_name=None): | def add_index(self, doctype, fields, index_name=None): | ||||
pass | |||||
raise NotImplementedError | |||||
def add_unique(self, doctype, fields, constraint_name=None): | def add_unique(self, doctype, fields, constraint_name=None): | ||||
pass | |||||
raise NotImplementedError | |||||
@staticmethod | @staticmethod | ||||
def get_index_name(fields): | def get_index_name(fields): | ||||
@@ -951,7 +951,7 @@ class Database(object): | |||||
def escape(s, percent=True): | def escape(s, percent=True): | ||||
"""Excape quotes and percent in given string.""" | """Excape quotes and percent in given string.""" | ||||
# implemented in specific class | # implemented in specific class | ||||
pass | |||||
raise NotImplementedError | |||||
@staticmethod | @staticmethod | ||||
def is_column_missing(e): | def is_column_missing(e): | ||||
@@ -22,11 +22,11 @@ class MariaDBDatabase(Database): | |||||
def setup_type_map(self): | def setup_type_map(self): | ||||
self.db_type = 'mariadb' | self.db_type = 'mariadb' | ||||
self.type_map = { | self.type_map = { | ||||
'Currency': ('decimal', '18,6'), | |||||
'Currency': ('decimal', '21,9'), | |||||
'Int': ('int', '11'), | 'Int': ('int', '11'), | ||||
'Long Int': ('bigint', '20'), | 'Long Int': ('bigint', '20'), | ||||
'Float': ('decimal', '18,6'), | |||||
'Percent': ('decimal', '18,6'), | |||||
'Float': ('decimal', '21,9'), | |||||
'Percent': ('decimal', '21,9'), | |||||
'Check': ('int', '1'), | 'Check': ('int', '1'), | ||||
'Small Text': ('text', ''), | 'Small Text': ('text', ''), | ||||
'Long Text': ('longtext', ''), | 'Long Text': ('longtext', ''), | ||||
@@ -51,7 +51,7 @@ class MariaDBDatabase(Database): | |||||
'Color': ('varchar', self.VARCHAR_LEN), | 'Color': ('varchar', self.VARCHAR_LEN), | ||||
'Barcode': ('longtext', ''), | 'Barcode': ('longtext', ''), | ||||
'Geolocation': ('longtext', ''), | 'Geolocation': ('longtext', ''), | ||||
'Duration': ('decimal', '18,6'), | |||||
'Duration': ('decimal', '21,9'), | |||||
'Icon': ('varchar', self.VARCHAR_LEN) | 'Icon': ('varchar', self.VARCHAR_LEN) | ||||
} | } | ||||
@@ -135,8 +135,8 @@ class MariaDBDatabase(Database): | |||||
table_name = get_table_name(doctype) | table_name = get_table_name(doctype) | ||||
return self.sql(f"DESC `{table_name}`") | return self.sql(f"DESC `{table_name}`") | ||||
def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]: | |||||
table_name = get_table_name(table) | |||||
def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]: | |||||
table_name = get_table_name(doctype) | |||||
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL") | return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL") | ||||
# exception types | # exception types | ||||
@@ -195,7 +195,7 @@ class MariaDBDatabase(Database): | |||||
`password` TEXT NOT NULL, | `password` TEXT NOT NULL, | ||||
`encrypted` INT(1) NOT NULL DEFAULT 0, | `encrypted` INT(1) NOT NULL DEFAULT 0, | ||||
PRIMARY KEY (`doctype`, `name`, `fieldname`) | PRIMARY KEY (`doctype`, `name`, `fieldname`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""") | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""") | |||||
def create_global_search_table(self): | def create_global_search_table(self): | ||||
if not '__global_search' in self.get_tables(): | if not '__global_search' in self.get_tables(): | ||||
@@ -72,7 +72,7 @@ CREATE TABLE `tabDocField` ( | |||||
KEY `label` (`label`), | KEY `label` (`label`), | ||||
KEY `fieldtype` (`fieldtype`), | KEY `fieldtype` (`fieldtype`), | ||||
KEY `fieldname` (`fieldname`) | KEY `fieldname` (`fieldname`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
@@ -109,7 +109,7 @@ CREATE TABLE `tabDocPerm` ( | |||||
`email` int(1) NOT NULL DEFAULT 1, | `email` int(1) NOT NULL DEFAULT 1, | ||||
PRIMARY KEY (`name`), | PRIMARY KEY (`name`), | ||||
KEY `parent` (`parent`) | KEY `parent` (`parent`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
-- Table structure for table `tabDocType Action` | -- Table structure for table `tabDocType Action` | ||||
@@ -133,7 +133,7 @@ CREATE TABLE `tabDocType Action` ( | |||||
PRIMARY KEY (`name`), | PRIMARY KEY (`name`), | ||||
KEY `parent` (`parent`), | KEY `parent` (`parent`), | ||||
KEY `modified` (`modified`) | KEY `modified` (`modified`) | ||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; | |||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; | |||||
-- | -- | ||||
-- Table structure for table `tabDocType Action` | -- Table structure for table `tabDocType Action` | ||||
@@ -156,7 +156,7 @@ CREATE TABLE `tabDocType Link` ( | |||||
PRIMARY KEY (`name`), | PRIMARY KEY (`name`), | ||||
KEY `parent` (`parent`), | KEY `parent` (`parent`), | ||||
KEY `modified` (`modified`) | KEY `modified` (`modified`) | ||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; | |||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; | |||||
-- | -- | ||||
-- Table structure for table `tabDocType` | -- Table structure for table `tabDocType` | ||||
@@ -228,7 +228,7 @@ CREATE TABLE `tabDocType` ( | |||||
`sender_field` varchar(255) DEFAULT NULL, | `sender_field` varchar(255) DEFAULT NULL, | ||||
PRIMARY KEY (`name`), | PRIMARY KEY (`name`), | ||||
KEY `parent` (`parent`) | KEY `parent` (`parent`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
-- Table structure for table `tabSeries` | -- Table structure for table `tabSeries` | ||||
@@ -239,7 +239,7 @@ CREATE TABLE `tabSeries` ( | |||||
`name` varchar(100), | `name` varchar(100), | ||||
`current` int(10) NOT NULL DEFAULT 0, | `current` int(10) NOT NULL DEFAULT 0, | ||||
PRIMARY KEY(`name`) | PRIMARY KEY(`name`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
@@ -256,7 +256,7 @@ CREATE TABLE `tabSessions` ( | |||||
`device` varchar(255) DEFAULT 'desktop', | `device` varchar(255) DEFAULT 'desktop', | ||||
`status` varchar(20) DEFAULT NULL, | `status` varchar(20) DEFAULT NULL, | ||||
KEY `sid` (`sid`) | KEY `sid` (`sid`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
@@ -269,7 +269,7 @@ CREATE TABLE `tabSingles` ( | |||||
`field` varchar(255) DEFAULT NULL, | `field` varchar(255) DEFAULT NULL, | ||||
`value` text, | `value` text, | ||||
KEY `singles_doctype_field_index` (`doctype`, `field`) | KEY `singles_doctype_field_index` (`doctype`, `field`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
-- Table structure for table `__Auth` | -- Table structure for table `__Auth` | ||||
@@ -283,7 +283,7 @@ CREATE TABLE `__Auth` ( | |||||
`password` TEXT NOT NULL, | `password` TEXT NOT NULL, | ||||
`encrypted` INT(1) NOT NULL DEFAULT 0, | `encrypted` INT(1) NOT NULL DEFAULT 0, | ||||
PRIMARY KEY (`doctype`, `name`, `fieldname`) | PRIMARY KEY (`doctype`, `name`, `fieldname`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
-- Table structure for table `tabFile` | -- Table structure for table `tabFile` | ||||
@@ -311,7 +311,7 @@ CREATE TABLE `tabFile` ( | |||||
KEY `parent` (`parent`), | KEY `parent` (`parent`), | ||||
KEY `attached_to_name` (`attached_to_name`), | KEY `attached_to_name` (`attached_to_name`), | ||||
KEY `attached_to_doctype` (`attached_to_doctype`) | KEY `attached_to_doctype` (`attached_to_doctype`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
-- | -- | ||||
-- Table structure for table `tabDefaultValue` | -- Table structure for table `tabDefaultValue` | ||||
@@ -334,4 +334,4 @@ CREATE TABLE `tabDefaultValue` ( | |||||
PRIMARY KEY (`name`), | PRIMARY KEY (`name`), | ||||
KEY `parent` (`parent`), | KEY `parent` (`parent`), | ||||
KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`) | KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`) | ||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
@@ -4,18 +4,22 @@ from frappe.database.schema import DBTable | |||||
class MariaDBTable(DBTable): | class MariaDBTable(DBTable): | ||||
def create(self): | def create(self): | ||||
add_text = '' | |||||
additional_definitions = "" | |||||
engine = self.meta.get("engine") or "InnoDB" | |||||
varchar_len = frappe.db.VARCHAR_LEN | |||||
# columns | # columns | ||||
column_defs = self.get_column_definitions() | column_defs = self.get_column_definitions() | ||||
if column_defs: add_text += ',\n'.join(column_defs) + ',\n' | |||||
if column_defs: | |||||
additional_definitions += ',\n'.join(column_defs) + ',\n' | |||||
# index | # index | ||||
index_defs = self.get_index_definitions() | index_defs = self.get_index_definitions() | ||||
if index_defs: add_text += ',\n'.join(index_defs) + ',\n' | |||||
if index_defs: | |||||
additional_definitions += ',\n'.join(index_defs) + ',\n' | |||||
# create table | # create table | ||||
frappe.db.sql("""create table `%s` ( | |||||
query = f"""create table `{self.table_name}` ( | |||||
name varchar({varchar_len}) not null primary key, | name varchar({varchar_len}) not null primary key, | ||||
creation datetime(6), | creation datetime(6), | ||||
modified datetime(6), | modified datetime(6), | ||||
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable): | |||||
parentfield varchar({varchar_len}), | parentfield varchar({varchar_len}), | ||||
parenttype varchar({varchar_len}), | parenttype varchar({varchar_len}), | ||||
idx int(8) not null default '0', | idx int(8) not null default '0', | ||||
%sindex parent(parent), | |||||
{additional_definitions} | |||||
index parent(parent), | |||||
index modified(modified)) | index modified(modified)) | ||||
ENGINE={engine} | ENGINE={engine} | ||||
ROW_FORMAT=COMPRESSED | |||||
ROW_FORMAT=DYNAMIC | |||||
CHARACTER SET=utf8mb4 | CHARACTER SET=utf8mb4 | ||||
COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN, | |||||
engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text)) | |||||
COLLATE=utf8mb4_unicode_ci""" | |||||
frappe.db.sql(query) | |||||
def alter(self): | def alter(self): | ||||
for col in self.columns.values(): | for col in self.columns.values(): | ||||
@@ -34,25 +34,23 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False): | |||||
db_name = frappe.local.conf.db_name | db_name = frappe.local.conf.db_name | ||||
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) | root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) | ||||
dbman = DbManager(root_conn) | dbman = DbManager(root_conn) | ||||
dbman_kwargs = {} | |||||
if no_mariadb_socket: | |||||
dbman_kwargs["host"] = "%" | |||||
if force or (db_name not in dbman.get_database_list()): | if force or (db_name not in dbman.get_database_list()): | ||||
dbman.delete_user(db_name) | |||||
if no_mariadb_socket: | |||||
dbman.delete_user(db_name, host="%") | |||||
dbman.delete_user(db_name, **dbman_kwargs) | |||||
dbman.drop_database(db_name) | dbman.drop_database(db_name) | ||||
else: | else: | ||||
raise Exception("Database %s already exists" % (db_name,)) | raise Exception("Database %s already exists" % (db_name,)) | ||||
dbman.create_user(db_name, frappe.conf.db_password) | |||||
if no_mariadb_socket: | |||||
dbman.create_user(db_name, frappe.conf.db_password, host="%") | |||||
dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs) | |||||
if verbose: print("Created user %s" % db_name) | if verbose: print("Created user %s" % db_name) | ||||
dbman.create_database(db_name) | dbman.create_database(db_name) | ||||
if verbose: print("Created database %s" % db_name) | if verbose: print("Created database %s" % db_name) | ||||
dbman.grant_all_privileges(db_name, db_name) | |||||
if no_mariadb_socket: | |||||
dbman.grant_all_privileges(db_name, db_name, host="%") | |||||
dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs) | |||||
dbman.flush_privileges() | dbman.flush_privileges() | ||||
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name)) | if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name)) | ||||
@@ -4,6 +4,7 @@ from typing import List, Tuple, Union | |||||
import psycopg2 | import psycopg2 | ||||
import psycopg2.extensions | import psycopg2.extensions | ||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT | from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT | ||||
from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION | |||||
import frappe | import frappe | ||||
from frappe.database.database import Database | from frappe.database.database import Database | ||||
@@ -31,11 +32,11 @@ class PostgresDatabase(Database): | |||||
def setup_type_map(self): | def setup_type_map(self): | ||||
self.db_type = 'postgres' | self.db_type = 'postgres' | ||||
self.type_map = { | self.type_map = { | ||||
'Currency': ('decimal', '18,6'), | |||||
'Currency': ('decimal', '21,9'), | |||||
'Int': ('bigint', None), | 'Int': ('bigint', None), | ||||
'Long Int': ('bigint', None), | 'Long Int': ('bigint', None), | ||||
'Float': ('decimal', '18,6'), | |||||
'Percent': ('decimal', '18,6'), | |||||
'Float': ('decimal', '21,9'), | |||||
'Percent': ('decimal', '21,9'), | |||||
'Check': ('smallint', None), | 'Check': ('smallint', None), | ||||
'Small Text': ('text', ''), | 'Small Text': ('text', ''), | ||||
'Long Text': ('text', ''), | 'Long Text': ('text', ''), | ||||
@@ -60,7 +61,7 @@ class PostgresDatabase(Database): | |||||
'Color': ('varchar', self.VARCHAR_LEN), | 'Color': ('varchar', self.VARCHAR_LEN), | ||||
'Barcode': ('text', ''), | 'Barcode': ('text', ''), | ||||
'Geolocation': ('text', ''), | 'Geolocation': ('text', ''), | ||||
'Duration': ('decimal', '18,6'), | |||||
'Duration': ('decimal', '21,9'), | |||||
'Icon': ('varchar', self.VARCHAR_LEN) | 'Icon': ('varchar', self.VARCHAR_LEN) | ||||
} | } | ||||
@@ -171,7 +172,7 @@ class PostgresDatabase(Database): | |||||
@staticmethod | @staticmethod | ||||
def is_data_too_long(e): | def is_data_too_long(e): | ||||
return e.pgcode == '22001' | |||||
return e.pgcode == STRING_DATA_RIGHT_TRUNCATION | |||||
def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]: | def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]: | ||||
old_name = get_table_name(old_name) | old_name = get_table_name(old_name) | ||||
@@ -182,8 +183,8 @@ class PostgresDatabase(Database): | |||||
table_name = get_table_name(doctype) | table_name = get_table_name(doctype) | ||||
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'") | return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'") | ||||
def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]: | |||||
table_name = get_table_name(table) | |||||
def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]: | |||||
table_name = get_table_name(doctype) | |||||
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}') | return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}') | ||||
def create_auth_table(self): | def create_auth_table(self): | ||||
@@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None): | |||||
size = d[1] if d[1] else None | size = d[1] if d[1] else None | ||||
if size: | if size: | ||||
# This check needs to exist for backward compatibility. | |||||
# Till V13, default size used for float, currency and percent are (18, 6). | |||||
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6: | if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6: | ||||
size = '21,9' | size = '21,9' | ||||
@@ -1,322 +1,106 @@ | |||||
{ | { | ||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 1, | |||||
"beta": 0, | |||||
"creation": "2013-05-24 13:41:00", | |||||
"custom": 0, | |||||
"description": "", | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"editable_grid": 0, | |||||
"fields": [ | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "title", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 1, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Title", | |||||
"length": 0, | |||||
"no_copy": 1, | |||||
"permlevel": 0, | |||||
"print_hide": 1, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "", | |||||
"fieldname": "public", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Public", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 1, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"depends_on": "public", | |||||
"fieldname": "notify_on_login", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Notify users with a popup when they log in", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"depends_on": "notify_on_login", | |||||
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", | |||||
"fieldname": "notify_on_every_login", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Notify Users On Every Login", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"depends_on": "eval:doc.notify_on_login && doc.public", | |||||
"fieldname": "expire_notification_on", | |||||
"fieldtype": "Date", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Expire Notification On", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 1, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")", | |||||
"fieldname": "content", | |||||
"fieldtype": "Text Editor", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 1, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Content", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 1, | |||||
"columns": 0, | |||||
"fieldname": "seen_by_section", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Seen By", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "seen_by", | |||||
"fieldtype": "Table", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Seen By Table", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Note Seen By", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
} | |||||
], | |||||
"has_web_view": 0, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"icon": "fa fa-file-text", | |||||
"idx": 1, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2018-09-21 15:15:44.909636", | |||||
"modified_by": "Administrator", | |||||
"module": "Desk", | |||||
"name": "Note", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "All", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 1, | |||||
"show_name_in_global_search": 0, | |||||
"sort_order": "ASC", | |||||
"track_changes": 1, | |||||
"track_seen": 0, | |||||
"track_views": 0 | |||||
} | |||||
"actions": [], | |||||
"allow_rename": 1, | |||||
"creation": "2013-05-24 13:41:00", | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"title", | |||||
"public", | |||||
"notify_on_login", | |||||
"notify_on_every_login", | |||||
"expire_notification_on", | |||||
"content", | |||||
"seen_by_section", | |||||
"seen_by" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "title", | |||||
"fieldtype": "Data", | |||||
"in_global_search": 1, | |||||
"in_list_view": 1, | |||||
"label": "Title", | |||||
"no_copy": 1, | |||||
"print_hide": 1, | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"default": "0", | |||||
"fieldname": "public", | |||||
"fieldtype": "Check", | |||||
"label": "Public", | |||||
"print_hide": 1 | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"default": "0", | |||||
"depends_on": "public", | |||||
"fieldname": "notify_on_login", | |||||
"fieldtype": "Check", | |||||
"label": "Notify users with a popup when they log in" | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"default": "0", | |||||
"depends_on": "notify_on_login", | |||||
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", | |||||
"fieldname": "notify_on_every_login", | |||||
"fieldtype": "Check", | |||||
"label": "Notify Users On Every Login" | |||||
}, | |||||
{ | |||||
"depends_on": "eval:doc.notify_on_login && doc.public", | |||||
"fieldname": "expire_notification_on", | |||||
"fieldtype": "Date", | |||||
"label": "Expire Notification On", | |||||
"search_index": 1 | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")", | |||||
"fieldname": "content", | |||||
"fieldtype": "Text Editor", | |||||
"in_global_search": 1, | |||||
"label": "Content" | |||||
}, | |||||
{ | |||||
"collapsible": 1, | |||||
"fieldname": "seen_by_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Seen By" | |||||
}, | |||||
{ | |||||
"fieldname": "seen_by", | |||||
"fieldtype": "Table", | |||||
"label": "Seen By Table", | |||||
"options": "Note Seen By" | |||||
} | |||||
], | |||||
"icon": "fa fa-file-text", | |||||
"idx": 1, | |||||
"links": [], | |||||
"modified": "2021-09-18 10:57:51.352643", | |||||
"modified_by": "Administrator", | |||||
"module": "Desk", | |||||
"name": "Note", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"role": "All", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"sort_field": "modified", | |||||
"sort_order": "ASC", | |||||
"track_changes": 1 | |||||
} |
@@ -10,15 +10,95 @@ frappe.ui.form.on('System Console', { | |||||
description: __('Execute Console script'), | description: __('Execute Console script'), | ||||
ignore_inputs: true, | ignore_inputs: true, | ||||
}); | }); | ||||
frm.set_value("type", "Python"); | |||||
}, | }, | ||||
refresh: function(frm) { | refresh: function(frm) { | ||||
frm.disable_save(); | frm.disable_save(); | ||||
frm.page.set_primary_action(__("Execute"), $btn => { | frm.page.set_primary_action(__("Execute"), $btn => { | ||||
$btn.text(__('Executing...')); | |||||
return frm.execute_action("Execute").then(() => { | |||||
$btn.text(__('Execute')); | |||||
}); | |||||
$btn.text(__("Executing...")); | |||||
return frm | |||||
.execute_action("Execute") | |||||
.then(() => frm.trigger("render_sql_output")) | |||||
.finally(() => $btn.text(__("Execute"))); | |||||
}); | |||||
}, | |||||
type: function(frm) { | |||||
if (frm.doc.type == "Python") { | |||||
frm.set_value("output", ""); | |||||
if (frm.sql_output) { | |||||
frm.sql_output.destroy(); | |||||
frm.get_field("sql_output").html(""); | |||||
} | |||||
} | |||||
}, | |||||
render_sql_output: function(frm) { | |||||
if (frm.doc.type !== "SQL") return; | |||||
if (frm.sql_output) { | |||||
frm.sql_output.destroy(); | |||||
frm.get_field("sql_output").html(""); | |||||
} | |||||
if (frm.doc.output.startsWith("Traceback")) { | |||||
return; | |||||
} | |||||
let result = JSON.parse(frm.doc.output); | |||||
frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`); | |||||
if (result.length) { | |||||
let columns = Object.keys(result[0]); | |||||
frm.sql_output = new DataTable( | |||||
frm.get_field("sql_output").$wrapper.get(0), | |||||
{ | |||||
columns, | |||||
data: result | |||||
} | |||||
); | |||||
} | |||||
}, | |||||
show_processlist: function(frm) { | |||||
if (frm.doc.show_processlist) { | |||||
// keep refreshing every 5 seconds | |||||
frm.events.refresh_processlist(frm); | |||||
frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000); | |||||
} else { | |||||
if (frm.processlist_interval) { | |||||
// end it | |||||
clearInterval(frm.processlist_interval); | |||||
frm.get_field("processlist").html(''); | |||||
} | |||||
} | |||||
}, | |||||
refresh_processlist: function(frm) { | |||||
let timestamp = new Date(); | |||||
frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => { | |||||
let rows = ''; | |||||
for (let row of r.message) { | |||||
rows += `<tr> | |||||
<td>${row.Id}</td> | |||||
<td>${row.Time}</td> | |||||
<td>${row.State}</td> | |||||
<td>${row.Info}</td> | |||||
<td>${row.Progress}</td> | |||||
</tr>` | |||||
} | |||||
frm.get_field('processlist').html(` | |||||
<p class='text-muted'>Requested on: ${timestamp}</p> | |||||
<table class='table-bordered' style='width: 100%'> | |||||
<thead><tr> | |||||
<th width='10%'>Id</ht> | |||||
<th width='10%'>Time</ht> | |||||
<th width='10%'>State</ht> | |||||
<th width='60%'>Info</ht> | |||||
<th width='10%'>Progress</ht> | |||||
</tr></thead> | |||||
<tbody>${rows}</thead>`); | |||||
}); | }); | ||||
} | } | ||||
}); | }); |
@@ -17,9 +17,15 @@ | |||||
"editable_grid": 1, | "editable_grid": 1, | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
"execute_section", | |||||
"type", | |||||
"console", | "console", | ||||
"commit", | "commit", | ||||
"output" | |||||
"output", | |||||
"sql_output", | |||||
"database_processes_section", | |||||
"show_processlist", | |||||
"processlist" | |||||
], | ], | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
@@ -40,13 +46,47 @@ | |||||
"fieldname": "commit", | "fieldname": "commit", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Commit" | "label": "Commit" | ||||
}, | |||||
{ | |||||
"fieldname": "execute_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Execute" | |||||
}, | |||||
{ | |||||
"fieldname": "database_processes_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Database Processes" | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "show_processlist", | |||||
"fieldtype": "Check", | |||||
"label": "Show Processlist" | |||||
}, | |||||
{ | |||||
"fieldname": "processlist", | |||||
"fieldtype": "HTML", | |||||
"label": "processlist" | |||||
}, | |||||
{ | |||||
"default": "Python", | |||||
"fieldname": "type", | |||||
"fieldtype": "Select", | |||||
"label": "Type", | |||||
"options": "Python\nSQL" | |||||
}, | |||||
{ | |||||
"depends_on": "eval:doc.type == 'SQL'", | |||||
"fieldname": "sql_output", | |||||
"fieldtype": "HTML", | |||||
"label": "SQL Output" | |||||
} | } | ||||
], | ], | ||||
"hide_toolbar": 1, | "hide_toolbar": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-08-21 14:44:35.296877", | |||||
"modified": "2021-09-15 17:17:44.844767", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "System Console", | "name": "System Console", | ||||
@@ -65,4 +105,4 @@ | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -5,7 +5,7 @@ | |||||
import json | import json | ||||
import frappe | import frappe | ||||
from frappe.utils.safe_exec import safe_exec | |||||
from frappe.utils.safe_exec import safe_exec, read_sql | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class SystemConsole(Document): | class SystemConsole(Document): | ||||
@@ -13,8 +13,11 @@ class SystemConsole(Document): | |||||
frappe.only_for('System Manager') | frappe.only_for('System Manager') | ||||
try: | try: | ||||
frappe.debug_log = [] | frappe.debug_log = [] | ||||
safe_exec(self.console) | |||||
self.output = '\n'.join(frappe.debug_log) | |||||
if self.type == 'Python': | |||||
safe_exec(self.console) | |||||
self.output = '\n'.join(frappe.debug_log) | |||||
elif self.type == 'SQL': | |||||
self.output = frappe.as_json(read_sql(self.console, as_dict=1)) | |||||
except: # noqa: E722 | except: # noqa: E722 | ||||
self.output = frappe.get_traceback() | self.output = frappe.get_traceback() | ||||
@@ -33,4 +36,9 @@ class SystemConsole(Document): | |||||
def execute_code(doc): | def execute_code(doc): | ||||
console = frappe.get_doc(json.loads(doc)) | console = frappe.get_doc(json.loads(doc)) | ||||
console.run() | console.run() | ||||
return console.as_dict() | |||||
return console.as_dict() | |||||
@frappe.whitelist() | |||||
def show_processlist(): | |||||
frappe.only_for('System Manager') | |||||
return frappe.db.sql('show full processlist', as_dict=1) |
@@ -128,46 +128,35 @@ def delete_tags_for_document(doc): | |||||
}) | }) | ||||
def update_tags(doc, tags): | def update_tags(doc, tags): | ||||
""" | |||||
Adds tags for documents | |||||
:param doc: Document to be added to global tags | |||||
""" | |||||
"""Adds tags for documents | |||||
:param doc: Document to be added to global tags | |||||
""" | |||||
new_tags = {tag.strip() for tag in tags.split(",") if tag} | new_tags = {tag.strip() for tag in tags.split(",") if tag} | ||||
for tag in new_tags: | |||||
if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}): | |||||
frappe.get_doc({ | |||||
"doctype": "Tag Link", | |||||
"document_type": doc.doctype, | |||||
"document_name": doc.name, | |||||
"parenttype": doc.doctype, | |||||
"parent": doc.name, | |||||
"title": doc.get_title() or '', | |||||
"tag": tag | |||||
}).insert(ignore_permissions=True) | |||||
existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={ | existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={ | ||||
"document_type": doc.doctype, | "document_type": doc.doctype, | ||||
"document_name": doc.name | "document_name": doc.name | ||||
}, fields=["tag"])] | }, fields=["tag"])] | ||||
deleted_tags = get_deleted_tags(new_tags, existing_tags) | |||||
if deleted_tags: | |||||
for tag in deleted_tags: | |||||
delete_tag_for_document(doc.doctype, doc.name, tag) | |||||
def get_deleted_tags(new_tags, existing_tags): | |||||
return list(set(existing_tags) - set(new_tags)) | |||||
def delete_tag_for_document(dt, dn, tag): | |||||
frappe.db.delete("Tag Link", { | |||||
"document_type": dt, | |||||
"document_name": dn, | |||||
"tag": tag | |||||
}) | |||||
added_tags = set(new_tags) - set(existing_tags) | |||||
for tag in added_tags: | |||||
frappe.get_doc({ | |||||
"doctype": "Tag Link", | |||||
"document_type": doc.doctype, | |||||
"document_name": doc.name, | |||||
"parenttype": doc.doctype, | |||||
"parent": doc.name, | |||||
"title": doc.get_title() or '', | |||||
"tag": tag | |||||
}).insert(ignore_permissions=True) | |||||
deleted_tags = list(set(existing_tags) - set(new_tags)) | |||||
for tag in deleted_tags: | |||||
frappe.db.delete("Tag Link", { | |||||
"document_type": doc.doctype, | |||||
"document_name": doc.name, | |||||
"tag": tag | |||||
}) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_documents_for_tag(tag): | def get_documents_for_tag(tag): | ||||
@@ -1,4 +1,5 @@ | |||||
{ | { | ||||
"actions": [], | |||||
"creation": "2019-09-24 13:25:36.435685", | "creation": "2019-09-24 13:25:36.435685", | ||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"editable_grid": 1, | "editable_grid": 1, | ||||
@@ -44,7 +45,8 @@ | |||||
"read_only": 1 | "read_only": 1 | ||||
} | } | ||||
], | ], | ||||
"modified": "2019-10-03 16:42:35.932409", | |||||
"links": [], | |||||
"modified": "2021-09-20 16:53:37.217998", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "Tag Link", | "name": "Tag Link", | ||||
@@ -61,6 +63,17 @@ | |||||
"role": "System Manager", | "role": "System Manager", | ||||
"share": 1, | "share": 1, | ||||
"write": 1 | "write": 1 | ||||
}, | |||||
{ | |||||
"create": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "All", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | } | ||||
], | ], | ||||
"read_only": 1, | "read_only": 1, | ||||
@@ -165,8 +165,6 @@ | |||||
"default": "0", | "default": "0", | ||||
"fieldname": "is_standard", | "fieldname": "is_standard", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Is Standard", | "label": "Is Standard", | ||||
"search_index": 1 | "search_index": 1 | ||||
}, | }, | ||||
@@ -181,7 +179,6 @@ | |||||
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user", | "depends_on": "eval:doc.extends_another_page == 1 || doc.for_user", | ||||
"fieldname": "extends", | "fieldname": "extends", | ||||
"fieldtype": "Link", | "fieldtype": "Link", | ||||
"in_standard_filter": 1, | |||||
"label": "Extends", | "label": "Extends", | ||||
"options": "Workspace", | "options": "Workspace", | ||||
"search_index": 1 | "search_index": 1 | ||||
@@ -228,6 +225,8 @@ | |||||
"default": "0", | "default": "0", | ||||
"fieldname": "public", | "fieldname": "public", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Public" | "label": "Public" | ||||
}, | }, | ||||
{ | { | ||||
@@ -265,11 +264,13 @@ | |||||
"label": "Roles" | "label": "Roles" | ||||
} | } | ||||
], | ], | ||||
"in_create": 1, | |||||
"links": [], | "links": [], | ||||
"modified": "2021-08-30 18:47:18.227154", | |||||
"modified": "2021-09-16 12:01:06.450621", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "Workspace", | "name": "Workspace", | ||||
"naming_rule": "By fieldname", | |||||
"owner": "Administrator", | "owner": "Administrator", | ||||
"permissions": [ | "permissions": [ | ||||
{ | { | ||||
@@ -208,17 +208,17 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de | |||||
if loads(deleted_pages): | if loads(deleted_pages): | ||||
return delete_pages(loads(deleted_pages)) | return delete_pages(loads(deleted_pages)) | ||||
return {"name": title, "public": public} | |||||
return {"name": title, "public": public, "label": doc.label} | |||||
def delete_pages(deleted_pages): | def delete_pages(deleted_pages): | ||||
for page in deleted_pages: | for page in deleted_pages: | ||||
if page.get("public") and "Workspace Manager" not in frappe.get_roles(): | if page.get("public") and "Workspace Manager" not in frappe.get_roles(): | ||||
return {"name": page.get("title"), "public": 1} | |||||
return {"name": page.get("title"), "public": 1, "label": page.get("label")} | |||||
if frappe.db.exists("Workspace", page.get("name")): | if frappe.db.exists("Workspace", page.get("name")): | ||||
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) | frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) | ||||
return {"name": "Home", "public": 1} | |||||
return {"name": "Home", "public": 1, "label": "Home"} | |||||
def sort_pages(sb_public_items, sb_private_items): | def sort_pages(sb_public_items, sb_private_items): | ||||
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) | wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) | ||||
@@ -121,7 +121,7 @@ def validate_filters(data, filters): | |||||
def setup_group_by(data): | def setup_group_by(data): | ||||
'''Add columns for aggregated values e.g. count(name)''' | '''Add columns for aggregated values e.g. count(name)''' | ||||
if data.group_by: | |||||
if data.group_by and data.aggregate_function: | |||||
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'): | if data.aggregate_function.lower() not in ('count', 'sum', 'avg'): | ||||
frappe.throw(_('Invalid aggregate function')) | frappe.throw(_('Invalid aggregate function')) | ||||
@@ -226,7 +226,7 @@ | |||||
}, | }, | ||||
{ | { | ||||
"default": "UNSEEN", | "default": "UNSEEN", | ||||
"depends_on": "eval: doc.enable_incoming", | |||||
"depends_on": "eval: doc.enable_incoming && doc.use_imap", | |||||
"fieldname": "email_sync_option", | "fieldname": "email_sync_option", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"hide_days": 1, | "hide_days": 1, | ||||
@@ -236,7 +236,7 @@ | |||||
}, | }, | ||||
{ | { | ||||
"default": "250", | "default": "250", | ||||
"depends_on": "eval: doc.enable_incoming", | |||||
"depends_on": "eval: doc.enable_incoming && doc.use_imap", | |||||
"description": "Total number of emails to sync in initial sync process ", | "description": "Total number of emails to sync in initial sync process ", | ||||
"fieldname": "initial_sync_count", | "fieldname": "initial_sync_count", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
@@ -567,7 +567,7 @@ | |||||
"icon": "fa fa-inbox", | "icon": "fa fa-inbox", | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2021-08-31 15:23:25.714366", | |||||
"modified": "2021-09-21 16:44:25.728637", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Email", | "module": "Email", | ||||
"name": "Email Account", | "name": "Email Account", | ||||
@@ -589,4 +589,4 @@ | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -146,6 +146,7 @@ def get_context(context): | |||||
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes: | if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes: | ||||
value = frappe.utils.cint(value) | value = frappe.utils.cint(value) | ||||
doc.reload() | |||||
doc.set(fieldname, value) | doc.set(fieldname, value) | ||||
doc.flags.updater_reference = { | doc.flags.updater_reference = { | ||||
'doctype': self.doctype, | 'doctype': self.doctype, | ||||
@@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase): | |||||
notification.event = 'Value Change' | notification.event = 'Value Change' | ||||
notification.value_changed = 'status' | notification.value_changed = 'status' | ||||
notification.send_to_all_assignees = 1 | notification.send_to_all_assignees = 1 | ||||
notification.set_property_after_alert = 'description' | |||||
notification.property_value = 'Changed by Notification' | |||||
notification.save() | notification.save() | ||||
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'): | if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'): | ||||
@@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase): | |||||
self.assertTrue(email_queue) | self.assertTrue(email_queue) | ||||
# check if description is changed after alert since set_property_after_alert is set | |||||
self.assertEquals(todo.description, 'Changed by Notification') | |||||
recipients = [d.recipient for d in email_queue.recipients] | recipients = [d.recipient for d in email_queue.recipients] | ||||
self.assertTrue('test2@example.com' in recipients) | self.assertTrue('test2@example.com' in recipients) | ||||
self.assertTrue('test1@example.com' in recipients) | self.assertTrue('test1@example.com' in recipients) | ||||
@@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site): | |||||
child_table = doc.get(df.fieldname) | child_table = doc.get(df.fieldname) | ||||
for entry in child_table: | for entry in child_table: | ||||
child_doc = producer_site.get_doc(entry.doctype, entry.name) | child_doc = producer_site.get_doc(entry.doctype, entry.name) | ||||
child_doc = frappe._dict(child_doc) | |||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site) | |||||
if child_doc: | |||||
child_doc = frappe._dict(child_doc) | |||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site) | |||||
def sync_link_dependencies(doc, link_fields, producer_site): | def sync_link_dependencies(doc, link_fields, producer_site): | ||||
set_dependencies(doc, link_fields, producer_site) | set_dependencies(doc, link_fields, producer_site) | ||||
@@ -223,7 +223,10 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | |||||
doc = frappe.get_doc(dt, dn) | doc = frappe.get_doc(dt, dn) | ||||
else: | else: | ||||
doc = frappe.get_doc(json.loads(docs)) | |||||
if isinstance(docs, str): | |||||
docs = json.loads(docs) | |||||
doc = frappe.get_doc(docs) | |||||
doc._original_modified = doc.modified | doc._original_modified = doc.modified | ||||
doc.check_if_latest() | doc.check_if_latest() | ||||
@@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe" | |||||
app_license = "MIT" | app_license = "MIT" | ||||
app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg' | app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg' | ||||
develop_version = '13.x.x-develop' | |||||
develop_version = '14.x.x-develop' | |||||
app_email = "info@frappe.io" | |||||
app_email = "developers@frappe.io" | |||||
docs_app = "frappe_io" | |||||
docs_app = "frappe_docs" | |||||
translator_url = "https://translate.erpnext.com" | translator_url = "https://translate.erpnext.com" | ||||
@@ -164,7 +164,8 @@ doc_events = { | |||||
"after_rename": "frappe.desk.notifications.clear_doctype_notifications", | "after_rename": "frappe.desk.notifications.clear_doctype_notifications", | ||||
"on_cancel": [ | "on_cancel": [ | ||||
"frappe.desk.notifications.clear_doctype_notifications", | "frappe.desk.notifications.clear_doctype_notifications", | ||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" | |||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", | |||||
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers" | |||||
], | ], | ||||
"on_trash": [ | "on_trash": [ | ||||
"frappe.desk.notifications.clear_doctype_notifications", | "frappe.desk.notifications.clear_doctype_notifications", | ||||
@@ -445,9 +445,21 @@ def extract_sql_from_archive(sql_file_path): | |||||
else: | else: | ||||
decompressed_file_name = sql_file_path | decompressed_file_name = sql_file_path | ||||
# convert archive sql to latest compatible | |||||
convert_archive_content(decompressed_file_name) | |||||
return decompressed_file_name | return decompressed_file_name | ||||
def convert_archive_content(sql_file_path): | |||||
if frappe.conf.db_type == "mariadb": | |||||
# ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed | |||||
# this step is added to ease restoring sites depending on older mariaDB servers | |||||
contents = open(sql_file_path).read() | |||||
with open(sql_file_path, "w") as f: | |||||
f.write(contents.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC")) | |||||
def extract_sql_gzip(sql_gz_path): | def extract_sql_gzip(sql_gz_path): | ||||
import subprocess | import subprocess | ||||
@@ -457,7 +469,7 @@ def extract_sql_gzip(sql_gz_path): | |||||
decompressed_file = original_file.rstrip(".gz") | decompressed_file = original_file.rstrip(".gz") | ||||
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file) | cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file) | ||||
subprocess.check_call(cmd, shell=True) | subprocess.check_call(cmd, shell=True) | ||||
except: | |||||
except Exception: | |||||
raise | raise | ||||
return decompressed_file | return decompressed_file | ||||
@@ -41,6 +41,7 @@ data_fieldtypes = ( | |||||
no_value_fields = ( | no_value_fields = ( | ||||
'Section Break', | 'Section Break', | ||||
'Column Break', | 'Column Break', | ||||
'Tab Break', | |||||
'HTML', | 'HTML', | ||||
'Table', | 'Table', | ||||
'Table MultiSelect', | 'Table MultiSelect', | ||||
@@ -53,6 +54,7 @@ no_value_fields = ( | |||||
display_fieldtypes = ( | display_fieldtypes = ( | ||||
'Section Break', | 'Section Break', | ||||
'Column Break', | 'Column Break', | ||||
'Tab Break', | |||||
'HTML', | 'HTML', | ||||
'Button', | 'Button', | ||||
'Image', | 'Image', | ||||
@@ -307,7 +307,7 @@ class BaseDocument(object): | |||||
doc["doctype"] = self.doctype | doc["doctype"] = self.doctype | ||||
for df in self.meta.get_table_fields(): | for df in self.meta.get_table_fields(): | ||||
children = self.get(df.fieldname) or [] | children = self.get(df.fieldname) or [] | ||||
doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children] | |||||
doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls, no_default_fields=no_default_fields) for d in children] | |||||
if no_nulls: | if no_nulls: | ||||
for k in list(doc): | for k in list(doc): | ||||
@@ -4,6 +4,7 @@ | |||||
from typing import List | from typing import List | ||||
import frappe.defaults | import frappe.defaults | ||||
from frappe.query_builder.utils import Column | |||||
import frappe.share | import frappe.share | ||||
from frappe import _ | from frappe import _ | ||||
import frappe.permissions | import frappe.permissions | ||||
@@ -491,7 +492,7 @@ class DatabaseQuery(object): | |||||
f.value = date_range | f.value = date_range | ||||
fallback = "'0001-01-01 00:00:00'" | fallback = "'0001-01-01 00:00:00'" | ||||
if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')): | |||||
if (f.fieldname in ('creation', 'modified')): | |||||
value = cstr(f.value) | value = cstr(f.value) | ||||
fallback = "NULL" | fallback = "NULL" | ||||
@@ -547,8 +548,12 @@ class DatabaseQuery(object): | |||||
value = flt(f.value) | value = flt(f.value) | ||||
fallback = 0 | fallback = 0 | ||||
if isinstance(f.value, Column): | |||||
quote = '"' if frappe.conf.db_type == 'postgres' else "`" | |||||
value = f"{tname}.{quote}{f.value.name}{quote}" | |||||
# escape value | # escape value | ||||
if isinstance(value, str) and not f.operator.lower() == 'between': | |||||
elif isinstance(value, str) and not f.operator.lower() == 'between': | |||||
value = f"{frappe.db.escape(value, percent=False)}" | value = f"{frappe.db.escape(value, percent=False)}" | ||||
if ( | if ( | ||||
@@ -15,6 +15,7 @@ Example: | |||||
''' | ''' | ||||
from datetime import datetime | from datetime import datetime | ||||
import click | |||||
import frappe, json, os | import frappe, json, os | ||||
from frappe.utils import cstr, cint, cast | from frappe.utils import cstr, cint, cast | ||||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields | from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields | ||||
@@ -658,27 +659,48 @@ def get_default_df(fieldname): | |||||
fieldtype = "Data" | fieldtype = "Data" | ||||
) | ) | ||||
def trim_tables(doctype=None): | |||||
def trim_tables(doctype=None, dry_run=False, quiet=False): | |||||
""" | """ | ||||
Removes database fields that don't exist in the doctype (json or custom field). This may be needed | Removes database fields that don't exist in the doctype (json or custom field). This may be needed | ||||
as maintenance since removing a field in a DocType doesn't automatically | as maintenance since removing a field in a DocType doesn't automatically | ||||
delete the db field. | delete the db field. | ||||
""" | """ | ||||
ignore_fields = default_fields + optional_fields | |||||
filters={ "issingle": 0 } | |||||
UPDATED_TABLES = {} | |||||
filters = {"issingle": 0} | |||||
if doctype: | if doctype: | ||||
filters["name"] = doctype | filters["name"] = doctype | ||||
for doctype in frappe.db.get_all("DocType", filters=filters): | |||||
doctype = doctype.name | |||||
columns = frappe.db.get_table_columns(doctype) | |||||
fields = frappe.get_meta(doctype).get_fieldnames_with_value() | |||||
columns_to_remove = [f for f in list(set(columns) - set(fields)) if f not in ignore_fields | |||||
and not f.startswith("_")] | |||||
if columns_to_remove: | |||||
print(doctype, "columns removed:", columns_to_remove) | |||||
columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove) | |||||
query = """alter table `tab{doctype}` {columns}""".format( | |||||
doctype=doctype, columns=columns_to_remove) | |||||
frappe.db.sql_ddl(query) | |||||
for doctype in frappe.db.get_all("DocType", filters=filters, pluck="name"): | |||||
try: | |||||
dropped_columns = trim_table(doctype, dry_run=dry_run) | |||||
if dropped_columns: | |||||
UPDATED_TABLES[doctype] = dropped_columns | |||||
except frappe.db.TableMissingError: | |||||
if quiet: | |||||
continue | |||||
click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True) | |||||
click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True) | |||||
except Exception as e: | |||||
if quiet: | |||||
continue | |||||
click.echo(e, err=True) | |||||
return UPDATED_TABLES | |||||
def trim_table(doctype, dry_run=True): | |||||
frappe.cache().hdel('table_columns', f"tab{doctype}") | |||||
ignore_fields = default_fields + optional_fields | |||||
columns = frappe.db.get_table_columns(doctype) | |||||
fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value() | |||||
is_internal = lambda f: f not in ignore_fields and not f.startswith("_") | |||||
columns_to_remove = [ | |||||
f for f in list(set(columns) - set(fields)) if is_internal(f) | |||||
] | |||||
DROPPED_COLUMNS = columns_to_remove[:] | |||||
if columns_to_remove and not dry_run: | |||||
columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove) | |||||
frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}") | |||||
return DROPPED_COLUMNS |
@@ -1,4 +1,4 @@ | |||||
import frappe | import frappe | ||||
def execute(): | def execute(): | ||||
frappe.db.change_column_type(table="__Auth", column="password", type="TEXT") | |||||
frappe.db.change_column_type("__Auth", column="password", type="TEXT") |
@@ -0,0 +1,29 @@ | |||||
// Copyright (c) 2021, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('Network Printer Settings', { | |||||
onload (frm) { | |||||
frm.trigger("connect_print_server"); | |||||
}, | |||||
server_ip (frm) { | |||||
frm.trigger("connect_print_server"); | |||||
}, | |||||
port (frm) { | |||||
frm.trigger("connect_print_server"); | |||||
}, | |||||
connect_print_server (frm) { | |||||
if (frm.doc.server_ip && frm.doc.port) { | |||||
frappe.call({ | |||||
"doc": frm.doc, | |||||
"method": "get_printers_list", | |||||
"args": { | |||||
ip: frm.doc.server_ip, | |||||
port: frm.doc.port | |||||
}, | |||||
callback: function(data) { | |||||
frm.set_df_property('printer_name', 'options', [""].concat(data.message)); | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
}); |
@@ -0,0 +1,66 @@ | |||||
{ | |||||
"actions": [], | |||||
"autoname": "Prompt", | |||||
"creation": "2021-09-17 11:26:06.943999", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"server_ip", | |||||
"port", | |||||
"column_break_4", | |||||
"printer_name" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"default": "localhost", | |||||
"fieldname": "server_ip", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Server IP", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"default": "631", | |||||
"fieldname": "port", | |||||
"fieldtype": "Int", | |||||
"in_list_view": 1, | |||||
"label": "Port", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_4", | |||||
"fieldtype": "Column Break" | |||||
}, | |||||
{ | |||||
"fieldname": "printer_name", | |||||
"fieldtype": "Select", | |||||
"label": "Printer Name", | |||||
"reqd": 1 | |||||
} | |||||
], | |||||
"index_web_pages_for_search": 1, | |||||
"links": [], | |||||
"modified": "2021-09-17 11:30:16.781655", | |||||
"modified_by": "Administrator", | |||||
"module": "Printing", | |||||
"name": "Network Printer Settings", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,37 @@ | |||||
# Copyright (c) 2021, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
from frappe import _ | |||||
class NetworkPrinterSettings(Document): | |||||
@frappe.whitelist() | |||||
def get_printers_list(self,ip="localhost",port=631): | |||||
printer_list = [] | |||||
try: | |||||
import cups | |||||
except ImportError: | |||||
frappe.throw(_('''This feature can not be used as dependencies are missing. | |||||
Please contact your system manager to enable this by installing pycups!''')) | |||||
return | |||||
try: | |||||
cups.setServer(self.server_ip) | |||||
cups.setPort(self.port) | |||||
conn = cups.Connection() | |||||
printers = conn.getPrinters() | |||||
for printer_id,printer in printers.items(): | |||||
printer_list.append({ | |||||
'value': printer_id, | |||||
'label': printer['printer-make-and-model'] | |||||
}) | |||||
except RuntimeError: | |||||
frappe.throw(_("Failed to connect to server")) | |||||
except frappe.ValidationError: | |||||
frappe.throw(_("Failed to connect to server")) | |||||
return printer_list | |||||
@frappe.whitelist() | |||||
def get_network_printer_settings(): | |||||
return frappe.db.get_list('Network Printer Settings', pluck='name') |
@@ -0,0 +1,8 @@ | |||||
# Copyright (c) 2021, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
# import frappe | |||||
import unittest | |||||
class TestNetworkPrinterSettings(unittest.TestCase): | |||||
pass |
@@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', { | |||||
}, | }, | ||||
onload: function(frm) { | onload: function(frm) { | ||||
frm.script_manager.trigger("print_style"); | frm.script_manager.trigger("print_style"); | ||||
}, | |||||
server_ip: function(frm) { | |||||
frm.trigger("connect_print_server"); | |||||
}, | |||||
port:function(frm) { | |||||
frm.trigger("connect_print_server"); | |||||
}, | |||||
connect_print_server:function(frm) { | |||||
if(frm.doc.server_ip && frm.doc.port){ | |||||
frappe.call({ | |||||
"doc": frm.doc, | |||||
"method": "get_printers", | |||||
"args": { | |||||
ip: frm.doc.server_ip, | |||||
port: frm.doc.port | |||||
}, | |||||
callback: function(data) { | |||||
frm.set_df_property('printer_name', 'options', [""].concat(data.message)); | |||||
}, | |||||
error: (data) => frm.set_value("enable_print_server", 0) | |||||
}); | |||||
} | |||||
} | } | ||||
}); | }); |
@@ -19,9 +19,6 @@ | |||||
"allow_print_for_cancelled", | "allow_print_for_cancelled", | ||||
"server_printer", | "server_printer", | ||||
"enable_print_server", | "enable_print_server", | ||||
"server_ip", | |||||
"printer_name", | |||||
"port", | |||||
"raw_printing_section", | "raw_printing_section", | ||||
"enable_raw_printing", | "enable_raw_printing", | ||||
"print_style_section", | "print_style_section", | ||||
@@ -107,29 +104,11 @@ | |||||
}, | }, | ||||
{ | { | ||||
"default": "0", | "default": "0", | ||||
"depends_on": "enable_print_server", | |||||
"fieldname": "enable_print_server", | "fieldname": "enable_print_server", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Enable Print Server" | |||||
}, | |||||
{ | |||||
"default": "localhost", | |||||
"depends_on": "enable_print_server", | |||||
"fieldname": "server_ip", | |||||
"fieldtype": "Data", | |||||
"label": "Server IP" | |||||
}, | |||||
{ | |||||
"depends_on": "enable_print_server", | |||||
"fieldname": "printer_name", | |||||
"fieldtype": "Select", | |||||
"label": "Printer Name" | |||||
}, | |||||
{ | |||||
"default": "631", | |||||
"depends_on": "enable_print_server", | |||||
"fieldname": "port", | |||||
"fieldtype": "Int", | |||||
"label": "Port" | |||||
"label": "Enable Print Server", | |||||
"mandatory_depends_on": "enable_print_server" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "raw_printing_section", | "fieldname": "raw_printing_section", | ||||
@@ -183,7 +162,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2021-02-15 14:16:18.474254", | |||||
"modified": "2021-09-17 12:59:14.783694", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Printing", | "module": "Printing", | ||||
"name": "Print Settings", | "name": "Print Settings", | ||||
@@ -12,26 +12,6 @@ class PrintSettings(Document): | |||||
def on_update(self): | def on_update(self): | ||||
frappe.clear_cache() | frappe.clear_cache() | ||||
@frappe.whitelist() | |||||
def get_printers(self,ip="localhost",port=631): | |||||
printer_list = [] | |||||
try: | |||||
import cups | |||||
except ImportError: | |||||
frappe.throw(_("You need to install pycups to use this feature!")) | |||||
return | |||||
try: | |||||
cups.setServer(self.server_ip) | |||||
cups.setPort(self.port) | |||||
conn = cups.Connection() | |||||
printers = conn.getPrinters() | |||||
printer_list = printers.keys() | |||||
except RuntimeError: | |||||
frappe.throw(_("Failed to connect to server")) | |||||
except frappe.ValidationError: | |||||
frappe.throw(_("Failed to connect to server")) | |||||
return printer_list | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def is_print_server_enabled(): | def is_print_server_enabled(): | ||||
if not hasattr(frappe.local, 'enable_print_server'): | if not hasattr(frappe.local, 'enable_print_server'): | ||||
@@ -165,10 +165,7 @@ frappe.ui.form.PrintView = class { | |||||
frappe.set_route('Form', 'Print Settings'); | frappe.set_route('Form', 'Print Settings'); | ||||
}); | }); | ||||
if ( | |||||
frappe.model.get_doc(':Print Settings', 'Print Settings') | |||||
.enable_raw_printing == '1' | |||||
) { | |||||
if (this.print_settings.enable_raw_printing == '1') { | |||||
this.page.add_menu_item(__('Raw Printing Setting'), () => { | this.page.add_menu_item(__('Raw Printing Setting'), () => { | ||||
this.printer_setting_dialog(); | this.printer_setting_dialog(); | ||||
}); | }); | ||||
@@ -179,6 +176,12 @@ frappe.ui.form.PrintView = class { | |||||
this.edit_print_format() | this.edit_print_format() | ||||
); | ); | ||||
} | } | ||||
if (this.print_settings.enable_print_server) { | |||||
this.page.add_menu_item(__('Select Network Printer'), () => | |||||
this.network_printer_setting_dialog() | |||||
); | |||||
} | |||||
} | } | ||||
show(frm) { | show(frm) { | ||||
@@ -460,72 +463,108 @@ frappe.ui.form.PrintView = class { | |||||
printit() { | printit() { | ||||
let me = this; | let me = this; | ||||
frappe.call({ | |||||
method: | |||||
'frappe.printing.doctype.print_settings.print_settings.is_print_server_enabled', | |||||
callback: function(data) { | |||||
if (data.message) { | |||||
frappe.call({ | |||||
method: 'frappe.utils.print_format.print_by_server', | |||||
args: { | |||||
doctype: me.frm.doc.doctype, | |||||
name: me.frm.doc.name, | |||||
print_format: me.selected_format(), | |||||
no_letterhead: me.with_letterhead(), | |||||
letterhead: this.get_letterhead(), | |||||
}, | |||||
callback: function() {}, | |||||
}); | |||||
} else if (me.get_mapped_printer().length === 1) { | |||||
// printer is already mapped in localstorage (applies for both raw and pdf ) | |||||
if (me.is_raw_printing()) { | |||||
me.get_raw_commands(function(out) { | |||||
frappe.ui.form | |||||
.qz_connect() | |||||
.then(function() { | |||||
let printer_map = me.get_mapped_printer()[0]; | |||||
let data = [out.raw_commands]; | |||||
let config = qz.configs.create(printer_map.printer); | |||||
return qz.print(config, data); | |||||
}) | |||||
.then(frappe.ui.form.qz_success) | |||||
.catch((err) => { | |||||
frappe.ui.form.qz_fail(err); | |||||
}); | |||||
if (me.print_settings.enable_print_server) { | |||||
if (localStorage.getItem('network_printer')) { | |||||
me.print_by_server(); | |||||
} else { | |||||
me.network_printer_setting_dialog(() => me.print_by_server()); | |||||
} | |||||
} else if (me.get_mapped_printer().length === 1) { | |||||
// printer is already mapped in localstorage (applies for both raw and pdf ) | |||||
if (me.is_raw_printing()) { | |||||
me.get_raw_commands(function(out) { | |||||
frappe.ui.form | |||||
.qz_connect() | |||||
.then(function() { | |||||
let printer_map = me.get_mapped_printer()[0]; | |||||
let data = [out.raw_commands]; | |||||
let config = qz.configs.create(printer_map.printer); | |||||
return qz.print(config, data); | |||||
}) | |||||
.then(frappe.ui.form.qz_success) | |||||
.catch((err) => { | |||||
frappe.ui.form.qz_fail(err); | |||||
}); | }); | ||||
} else { | |||||
frappe.show_alert( | |||||
}); | |||||
} else { | |||||
frappe.show_alert( | |||||
{ | |||||
message: __('PDF printing via "Raw Print" is not supported.'), | |||||
subtitle: __( | |||||
'Please remove the printer mapping in Printer Settings and try again.' | |||||
), | |||||
indicator: 'info', | |||||
}, | |||||
14 | |||||
); | |||||
//Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing. | |||||
} | |||||
} else if (me.is_raw_printing()) { | |||||
// printer not mapped in localstorage and the current print format is raw printing | |||||
frappe.show_alert( | |||||
{ | |||||
message: __('Printer mapping not set.'), | |||||
subtitle: __( | |||||
'Please set a printer mapping for this print format in the Printer Settings' | |||||
), | |||||
indicator: 'warning', | |||||
}, | |||||
14 | |||||
); | |||||
me.printer_setting_dialog(); | |||||
} else { | |||||
me.render_page('/printview?', true); | |||||
} | |||||
} | |||||
print_by_server() { | |||||
let me = this; | |||||
if (localStorage.getItem('network_printer')) { | |||||
frappe.call({ | |||||
method: 'frappe.utils.print_format.print_by_server', | |||||
args: { | |||||
doctype: me.frm.doc.doctype, | |||||
name: me.frm.doc.name, | |||||
printer_setting: localStorage.getItem('network_printer'), | |||||
print_format: me.selected_format(), | |||||
no_letterhead: me.with_letterhead(), | |||||
letterhead: me.get_letterhead(), | |||||
}, | |||||
callback: function() {}, | |||||
}); | |||||
} | |||||
} | |||||
network_printer_setting_dialog(callback) { | |||||
frappe.call({ | |||||
method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings', | |||||
callback: function(r) { | |||||
if (r.message) { | |||||
let d = new frappe.ui.Dialog({ | |||||
title: __('Select Network Printer'), | |||||
fields: [ | |||||
{ | { | ||||
message: __('PDF printing via "Raw Print" is not supported.'), | |||||
subtitle: __( | |||||
'Please remove the printer mapping in Printer Settings and try again.' | |||||
), | |||||
indicator: 'info', | |||||
}, | |||||
14 | |||||
); | |||||
//Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing. | |||||
} | |||||
} else if (me.is_raw_printing()) { | |||||
// printer not mapped in localstorage and the current print format is raw printing | |||||
frappe.show_alert( | |||||
{ | |||||
message: __('Printer mapping not set.'), | |||||
subtitle: __( | |||||
'Please set a printer mapping for this print format in the Printer Settings' | |||||
), | |||||
indicator: 'warning', | |||||
"label": "Printer", | |||||
"fieldname": "printer", | |||||
"fieldtype": "Select", | |||||
"reqd": 1, | |||||
"options": r.message | |||||
} | |||||
], | |||||
primary_action: function() { | |||||
localStorage.setItem('network_printer', d.get_values().printer); | |||||
if (typeof callback == "function") { | |||||
callback(); | |||||
} | |||||
d.hide(); | |||||
}, | }, | ||||
14 | |||||
); | |||||
me.printer_setting_dialog(); | |||||
} else { | |||||
me.render_page('/printview?', true); | |||||
primary_action_label: __('Select') | |||||
}); | |||||
d.show(); | |||||
} | } | ||||
}, | }, | ||||
}); | }); | ||||
} | } | ||||
render_page(method, printit = false) { | render_page(method, printit = false) { | ||||
let w = window.open( | let w = window.open( | ||||
frappe.urllib.get_full_url( | frappe.urllib.get_full_url( | ||||
@@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { | |||||
} else if(f.fieldtype==="Column Break") { | } else if(f.fieldtype==="Column Break") { | ||||
set_column(); | set_column(); | ||||
} else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype) | |||||
} else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype) | |||||
&& f.label) { | && f.label) { | ||||
if(!column) set_column(); | if(!column) set_column(); | ||||
@@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { | |||||
init_visible_columns(f) { | init_visible_columns(f) { | ||||
f.visible_columns = [] | f.visible_columns = [] | ||||
$.each(frappe.get_meta(f.options).fields, function(i, _f) { | $.each(frappe.get_meta(f.options).fields, function(i, _f) { | ||||
if(!in_list(["Section Break", "Column Break"], _f.fieldtype) && | |||||
if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) && | |||||
!_f.print_hide && f.label) { | !_f.print_hide && f.label) { | ||||
// column names set as fieldname|width | // column names set as fieldname|width | ||||
@@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { | |||||
// add remaining fields | // add remaining fields | ||||
$.each(doc_fields, function(j, f) { | $.each(doc_fields, function(j, f) { | ||||
if (f && !in_list(column_names, f.fieldname) | if (f && !in_list(column_names, f.fieldname) | ||||
&& !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) { | |||||
&& !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) { | |||||
fields.push(f); | fields.push(f); | ||||
} | } | ||||
}) | }) | ||||
@@ -4,7 +4,7 @@ | |||||
</div> | </div> | ||||
<div class="print-format-builder-sidebar-fields"> | <div class="print-format-builder-sidebar-fields"> | ||||
{% for (var i=0, l=fields.length; i < l; i++) { var f = fields[i]; %} | {% for (var i=0, l=fields.length; i < l; i++) { var f = fields[i]; %} | ||||
{% if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)) { %} | |||||
{% if(!in_list(["Section Break", "Tab Break", "Column Break", "Fold"], f.fieldtype)) { %} | |||||
<div class="print-format-builder-field-placeholder" | <div class="print-format-builder-field-placeholder" | ||||
data-fieldname="{%= f.fieldname %}"> | data-fieldname="{%= f.fieldname %}"> | ||||
<div title="{{f.label}}" class="field-label btn btn-default btn-sm sidebar-field ellipsis | <div title="{{f.label}}" class="field-label btn btn-default btn-sm sidebar-field ellipsis | ||||
@@ -30,6 +30,9 @@ import "./frappe/ui/slides.js"; | |||||
import "./frappe/ui/find.js"; | import "./frappe/ui/find.js"; | ||||
import "./frappe/ui/iconbar.js"; | import "./frappe/ui/iconbar.js"; | ||||
import "./frappe/form/layout.js"; | import "./frappe/form/layout.js"; | ||||
import "./frappe/form/section.js"; | |||||
import "./frappe/form/tab.js"; | |||||
import "./frappe/form/column.js"; | |||||
import "./frappe/ui/field_group.js"; | import "./frappe/ui/field_group.js"; | ||||
import "./frappe/form/link_selector.js"; | import "./frappe/form/link_selector.js"; | ||||
import "./frappe/form/multi_select_dialog.js"; | import "./frappe/form/multi_select_dialog.js"; | ||||
@@ -0,0 +1,49 @@ | |||||
export default class Column { | |||||
constructor(section, df) { | |||||
if (!df) df = {}; | |||||
this.df = df; | |||||
this.section = section; | |||||
this.make(); | |||||
this.resize_all_columns(); | |||||
} | |||||
make() { | |||||
this.wrapper = $(` | |||||
<div class="form-column"> | |||||
<form> | |||||
</form> | |||||
</div> | |||||
`) | |||||
.appendTo(this.section.body) | |||||
.find("form") | |||||
.on("submit", function () { | |||||
return false; | |||||
}); | |||||
if (this.df.label) { | |||||
$(` | |||||
<label class="control-label"> | |||||
${__(this.df.label)} | |||||
</label> | |||||
`) | |||||
.appendTo(this.wrapper); | |||||
} | |||||
} | |||||
resize_all_columns() { | |||||
// distribute all columns equally | |||||
let colspan = cint(12 / this.section.wrapper.find(".form-column").length); | |||||
this.section.wrapper | |||||
.find(".form-column") | |||||
.removeClass() | |||||
.addClass("form-column") | |||||
.addClass("col-sm-" + colspan); | |||||
} | |||||
refresh() { | |||||
this.section.refresh(); | |||||
} | |||||
} |
@@ -1,4 +1,17 @@ | |||||
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { | frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { | ||||
make_input() { | |||||
super.make_input(); | |||||
const change_handler = e => { | |||||
if (this.change) this.change(e); | |||||
else { | |||||
let value = this.get_input_value(); | |||||
this.parse_validate_and_set_in_model(value, e); | |||||
} | |||||
}; | |||||
// convert to number format on focusout since focus converts it to flt. | |||||
this.$input.on("focusout", change_handler); | |||||
} | |||||
parse(value) { | parse(value) { | ||||
value = this.eval_expression(value); | value = this.eval_expression(value); | ||||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | ||||
@@ -1,61 +1,65 @@ | |||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
// MIT License. See license.txt | // MIT License. See license.txt | ||||
import Section from "./section.js"; | |||||
frappe.ui.form.Dashboard = class FormDashboard { | frappe.ui.form.Dashboard = class FormDashboard { | ||||
constructor(opts) { | |||||
$.extend(this, opts); | |||||
constructor(parent, frm) { | |||||
this.parent = parent; | |||||
this.frm = frm; | |||||
this.setup_dashboard_sections(); | this.setup_dashboard_sections(); | ||||
} | } | ||||
setup_dashboard_sections() { | setup_dashboard_sections() { | ||||
this.progress_area = new Section(this.parent, { | |||||
this.progress_area = this.make_section({ | |||||
css_class: 'progress-area', | css_class: 'progress-area', | ||||
hidden: 1, | hidden: 1, | ||||
collapsible: 1 | |||||
is_dashboard_section: 1, | |||||
}); | }); | ||||
this.heatmap_area = new Section(this.parent, { | |||||
title: __("Overview"), | |||||
this.heatmap_area = this.make_section({ | |||||
label: __("Overview"), | |||||
css_class: 'form-heatmap', | css_class: 'form-heatmap', | ||||
hidden: 1, | hidden: 1, | ||||
collapsible: 1, | |||||
is_dashboard_section: 1, | |||||
body_html: ` | body_html: ` | ||||
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div> | <div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div> | ||||
<div class="text-muted small heatmap-message hidden"></div> | <div class="text-muted small heatmap-message hidden"></div> | ||||
` | ` | ||||
}); | }); | ||||
this.chart_area = new Section(this.parent, { | |||||
title: __("Graph"), | |||||
this.chart_area = this.make_section({ | |||||
label: __("Graph"), | |||||
css_class: 'form-graph', | css_class: 'form-graph', | ||||
hidden: 1, | hidden: 1, | ||||
collapsible: 1 | |||||
is_dashboard_section: 1 | |||||
}); | }); | ||||
this.stats_area_row = $(`<div class="row"></div>`); | this.stats_area_row = $(`<div class="row"></div>`); | ||||
this.stats_area = new Section(this.parent, { | |||||
title: __("Stats"), | |||||
this.stats_area = this.make_section({ | |||||
label: __("Stats"), | |||||
css_class: 'form-stats', | css_class: 'form-stats', | ||||
hidden: 1, | hidden: 1, | ||||
collapsible: 1, | |||||
is_dashboard_section: 1, | |||||
body_html: this.stats_area_row | body_html: this.stats_area_row | ||||
}); | }); | ||||
this.transactions_area = $(`<div class="transactions"></div`); | this.transactions_area = $(`<div class="transactions"></div`); | ||||
this.links_area = new Section(this.parent, { | |||||
title: __("Connections"), | |||||
this.links_area = this.make_section({ | |||||
label: __("Connections"), | |||||
css_class: 'form-links', | css_class: 'form-links', | ||||
hidden: 1, | hidden: 1, | ||||
collapsible: 1, | |||||
is_dashboard_section: 1, | |||||
body_html: this.transactions_area | body_html: this.transactions_area | ||||
}); | }); | ||||
} | |||||
make_section(df) { | |||||
return new Section(this.parent, df); | |||||
} | } | ||||
reset() { | reset() { | ||||
this.hide(); | |||||
// clear progress | // clear progress | ||||
this.progress_area.body.empty(); | this.progress_area.body.empty(); | ||||
this.progress_area.hide(); | this.progress_area.hide(); | ||||
@@ -70,19 +74,19 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
// clear custom | // clear custom | ||||
this.parent.find('.custom').remove(); | this.parent.find('.custom').remove(); | ||||
this.hide(); | |||||
// this.hide(); | |||||
} | } | ||||
add_section(body_html, title=null, css_class="custom", hidden=false) { | |||||
add_section(body_html, label=null, css_class="custom", hidden=false) { | |||||
let options = { | let options = { | ||||
title, | |||||
label, | |||||
css_class, | css_class, | ||||
hidden, | hidden, | ||||
body_html, | body_html, | ||||
make_card: true, | make_card: true, | ||||
collapsible: 1 | |||||
is_dashboard_section: 1 | |||||
}; | }; | ||||
return new Section(this.parent, options).body; | |||||
return new Section(this.frm.layout.wrapper, options).body; | |||||
} | } | ||||
add_progress(title, percent, message) { | add_progress(title, percent, message) { | ||||
@@ -154,7 +158,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
make_progress_chart(title) { | make_progress_chart(title) { | ||||
this.progress_area.show(); | this.progress_area.show(); | ||||
var progress_chart = $('<div class="progress-chart" title="'+(title || '')+'"></div>') | |||||
let progress_chart = $('<div class="progress-chart" title="'+(title || '')+'"></div>') | |||||
.appendTo(this.progress_area.body); | .appendTo(this.progress_area.body); | ||||
return progress_chart; | return progress_chart; | ||||
} | } | ||||
@@ -169,7 +173,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
this.init_data(); | this.init_data(); | ||||
} | } | ||||
var show = false; | |||||
let show = false; | |||||
if (this.data && ((this.data.transactions || []).length | if (this.data && ((this.data.transactions || []).length | ||||
|| (this.data.reports || []).length)) { | || (this.data.reports || []).length)) { | ||||
@@ -197,11 +201,10 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
} | } | ||||
after_refresh() { | after_refresh() { | ||||
var me = this; | |||||
// show / hide new buttons (if allowed) | // show / hide new buttons (if allowed) | ||||
this.links_area.body.find('.btn-new').each(function() { | |||||
if (me.frm.can_create($(this).attr('data-doctype'))) { | |||||
$(this).removeClass('hidden'); | |||||
this.links_area.body.find('.btn-new').each((i, el) => { | |||||
if (this.frm.can_create($(this).attr('data-doctype'))) { | |||||
$(el).removeClass('hidden'); | |||||
} | } | ||||
}); | }); | ||||
!this.frm.is_new() && this.set_open_count(); | !this.frm.is_new() && this.set_open_count(); | ||||
@@ -269,7 +272,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
} | } | ||||
render_links() { | render_links() { | ||||
var me = this; | |||||
let me = this; | |||||
this.links_area.show(); | this.links_area.show(); | ||||
this.links_area.body.find('.btn-new').addClass('hidden'); | this.links_area.body.find('.btn-new').addClass('hidden'); | ||||
if (this.data_rendered) { | if (this.data_rendered) { | ||||
@@ -329,7 +332,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
open_document_list($link, show_open) { | open_document_list($link, show_open) { | ||||
// show document list with filters | // show document list with filters | ||||
var doctype = $link.attr('data-doctype'), | |||||
let doctype = $link.attr('data-doctype'), | |||||
names = $link.attr('data-names') || []; | names = $link.attr('data-names') || []; | ||||
if (this.data.internal_links[doctype]) { | if (this.data.internal_links[doctype]) { | ||||
@@ -351,8 +354,8 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
get_document_filter(doctype) { | get_document_filter(doctype) { | ||||
// return the default filter for the given document | // return the default filter for the given document | ||||
// like {"customer": frm.doc.name} | // like {"customer": frm.doc.name} | ||||
var filter = {}; | |||||
var fieldname = this.data.non_standard_fieldnames | |||||
let filter = {}; | |||||
let fieldname = this.data.non_standard_fieldnames | |||||
? (this.data.non_standard_fieldnames[doctype] || this.data.fieldname) | ? (this.data.non_standard_fieldnames[doctype] || this.data.fieldname) | ||||
: this.data.fieldname; | : this.data.fieldname; | ||||
@@ -371,7 +374,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
} | } | ||||
// list all items from the transaction list | // list all items from the transaction list | ||||
var items = [], | |||||
let items = [], | |||||
me = this; | me = this; | ||||
this.data.transactions.forEach(function(group) { | this.data.transactions.forEach(function(group) { | ||||
@@ -380,7 +383,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
}); | }); | ||||
}); | }); | ||||
var method = this.data.method || 'frappe.desk.notifications.get_open_count'; | |||||
let method = this.data.method || 'frappe.desk.notifications.get_open_count'; | |||||
frappe.call({ | frappe.call({ | ||||
type: "GET", | type: "GET", | ||||
method: method, | method: method, | ||||
@@ -429,7 +432,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
} | } | ||||
set_badge_count(doctype, open_count, count, names) { | set_badge_count(doctype, open_count, count, names) { | ||||
var $link = $(this.transactions_area) | |||||
let $link = $(this.transactions_area) | |||||
.find('.document-link[data-doctype="'+doctype+'"]'); | .find('.document-link[data-doctype="'+doctype+'"]'); | ||||
if (open_count) { | if (open_count) { | ||||
@@ -476,7 +479,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
this.heatmap_area.body.find('svg').css({'margin': 'auto'}); | this.heatmap_area.body.find('svg').css({'margin': 'auto'}); | ||||
// message | // message | ||||
var heatmap_message = this.heatmap_area.body.find('.heatmap-message'); | |||||
let heatmap_message = this.heatmap_area.body.find('.heatmap-message'); | |||||
if (this.data.heatmap_message) { | if (this.data.heatmap_message) { | ||||
heatmap_message.removeClass('hidden').html(this.data.heatmap_message); | heatmap_message.removeClass('hidden').html(this.data.heatmap_message); | ||||
} else { | } else { | ||||
@@ -491,9 +494,9 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
// set colspan | // set colspan | ||||
var indicators = this.stats_area_row.find('.indicator-column'); | |||||
var n_indicators = indicators.length + 1; | |||||
var colspan; | |||||
let indicators = this.stats_area_row.find('.indicator-column'); | |||||
let n_indicators = indicators.length + 1; | |||||
let colspan; | |||||
if (n_indicators > 4) { | if (n_indicators > 4) { | ||||
colspan = 3; | colspan = 3; | ||||
} else { | } else { | ||||
@@ -505,7 +508,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
indicators.removeClass().addClass('col-sm-'+colspan).addClass('indicator-column'); | indicators.removeClass().addClass('col-sm-'+colspan).addClass('indicator-column'); | ||||
} | } | ||||
var indicator = $('<div class="col-sm-'+colspan+' indicator-column"><span class="indicator '+color+'">' | |||||
let indicator = $('<div class="col-sm-'+colspan+' indicator-column"><span class="indicator '+color+'">' | |||||
+label+'</span></div>').appendTo(this.stats_area_row); | +label+'</span></div>').appendTo(this.stats_area_row); | ||||
return indicator; | return indicator; | ||||
@@ -513,9 +516,9 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
// graphs | // graphs | ||||
setup_graph() { | setup_graph() { | ||||
var me = this; | |||||
var method = this.data.graph_method; | |||||
var args = { | |||||
let me = this; | |||||
let method = this.data.graph_method; | |||||
let args = { | |||||
doctype: this.frm.doctype, | doctype: this.frm.doctype, | ||||
docname: this.frm.doc.name, | docname: this.frm.doc.name, | ||||
}; | }; | ||||
@@ -579,11 +582,10 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
} | } | ||||
add_comment(text, alert_class, permanent) { | add_comment(text, alert_class, permanent) { | ||||
var me = this; | |||||
this.set_headline_alert(text, alert_class); | this.set_headline_alert(text, alert_class); | ||||
if (!permanent) { | if (!permanent) { | ||||
setTimeout(function() { | |||||
me.clear_headline(); | |||||
setTimeout(() => { | |||||
this.clear_headline(); | |||||
}, 10000); | }, 10000); | ||||
} | } | ||||
} | } | ||||
@@ -600,109 +602,3 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||||
} | } | ||||
} | } | ||||
}; | }; | ||||
class Section { | |||||
constructor(parent, options) { | |||||
this.parent = parent; | |||||
this.df = options || {}; | |||||
this.make(); | |||||
if (this.df.title && this.df.collapsible && localStorage.getItem(options.css_class + '-closed')) { | |||||
this.collapse(); | |||||
} | |||||
this.refresh(); | |||||
} | |||||
make() { | |||||
this.wrapper = $(`<div class="form-dashboard-section ${ this.df.make_card ? "card-section" : "" }">`) | |||||
.appendTo(this.parent); | |||||
if (this.df) { | |||||
if (this.df.title) { | |||||
this.make_head(); | |||||
} | |||||
if (this.df.description) { | |||||
this.description_wrapper = $( | |||||
`<div class="col-sm-12 form-section-description"> | |||||
${__(this.df.description)} | |||||
</div>` | |||||
); | |||||
this.wrapper.append(this.description_wrapper); | |||||
} | |||||
if (this.df.css_class) { | |||||
this.wrapper.addClass(this.df.css_class); | |||||
} | |||||
if (this.df.hide_border) { | |||||
this.wrapper.toggleClass("hide-border", true); | |||||
} | |||||
} | |||||
this.body = $('<div class="section-body">').appendTo(this.wrapper); | |||||
if (this.df.body_html) { | |||||
this.body.append(this.df.body_html); | |||||
} | |||||
} | |||||
make_head() { | |||||
this.head = $(` | |||||
<div class="section-head"> | |||||
${__(this.df.title)} | |||||
<span class="ml-2 collapse-indicator mb-1"></span> | |||||
</div> | |||||
`); | |||||
this.head.appendTo(this.wrapper); | |||||
this.indicator = this.head.find('.collapse-indicator'); | |||||
this.indicator.hide(); | |||||
if (this.df.collapsible) { | |||||
// show / hide based on status | |||||
this.collapse_link = this.head.on("click", () => { | |||||
this.collapse(); | |||||
}); | |||||
this.set_icon(); | |||||
this.indicator.show(); | |||||
} | |||||
} | |||||
refresh() { | |||||
if (!this.df) return; | |||||
// hide if explicitly hidden | |||||
let hide = this.df.hidden; | |||||
this.wrapper.toggle(!hide); | |||||
} | |||||
collapse(hide) { | |||||
if (hide === undefined) { | |||||
hide = !this.body.hasClass("hide"); | |||||
} | |||||
this.body.toggleClass("hide", hide); | |||||
this.head && this.head.toggleClass("collapsed", hide); | |||||
this.set_icon(hide); | |||||
// save state for next reload ('' is falsy) | |||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : ''); | |||||
} | |||||
set_icon(hide) { | |||||
let indicator_icon = hide ? 'down' : 'up-line'; | |||||
this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); | |||||
} | |||||
is_collapsed() { | |||||
return this.body.hasClass('hide'); | |||||
} | |||||
hide() { | |||||
this.wrapper.hide(); | |||||
} | |||||
show() { | |||||
this.wrapper.show(); | |||||
} | |||||
} |
@@ -147,7 +147,9 @@ class FormTimeline extends BaseTimeline { | |||||
} | } | ||||
get_user_link(user) { | get_user_link(user) { | ||||
const user_display_text = (frappe.user_info(user).fullname || '').bold(); | |||||
const user_display_text = ( | |||||
(frappe.session.user == user ? __("You") : frappe.user_info(user).fullname) || '' | |||||
).bold(); | |||||
return frappe.utils.get_form_link('User', user, true, user_display_text); | return frappe.utils.get_form_link('User', user, true, user_display_text); | ||||
} | } | ||||
@@ -353,7 +355,7 @@ class FormTimeline extends BaseTimeline { | |||||
icon: 'branch', | icon: 'branch', | ||||
icon_size: 'sm', | icon_size: 'sm', | ||||
creation: workflow_log.creation, | creation: workflow_log.creation, | ||||
content: __(workflow_log.content), | |||||
content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`, | |||||
title: "Workflow", | title: "Workflow", | ||||
}); | }); | ||||
}); | }); | ||||
@@ -94,6 +94,11 @@ frappe.ui.form.Form = class FrappeForm { | |||||
this.watch_model_updates(); | this.watch_model_updates(); | ||||
if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { | if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { | ||||
// this.footer_tab = new frappe.ui.form.Tab(this.layout, { | |||||
// label: __("Activity"), | |||||
// fieldname: 'timeline' | |||||
// }); | |||||
this.footer = new frappe.ui.form.Footer({ | this.footer = new frappe.ui.form.Footer({ | ||||
frm: this, | frm: this, | ||||
parent: $('<div>').appendTo(this.page.main.parent()) | parent: $('<div>').appendTo(this.page.main.parent()) | ||||
@@ -128,8 +133,8 @@ frappe.ui.form.Form = class FrappeForm { | |||||
} | } | ||||
setup_std_layout() { | setup_std_layout() { | ||||
this.form_wrapper = $('<div></div>').appendTo(this.layout_main); | |||||
this.body = $('<div></div>').appendTo(this.form_wrapper); | |||||
this.form_wrapper = $('<div></div>').appendTo(this.layout_main); | |||||
this.body = $('<div></div>').appendTo(this.form_wrapper); | |||||
// only tray | // only tray | ||||
this.meta.section_style='Simple'; // always simple! | this.meta.section_style='Simple'; // always simple! | ||||
@@ -141,17 +146,19 @@ frappe.ui.form.Form = class FrappeForm { | |||||
doctype_layout: this.doctype_layout, | doctype_layout: this.doctype_layout, | ||||
frm: this, | frm: this, | ||||
with_dashboard: true, | with_dashboard: true, | ||||
card_layout: true, | |||||
card_layout: true | |||||
}); | }); | ||||
this.layout.make(); | this.layout.make(); | ||||
this.fields_dict = this.layout.fields_dict; | this.fields_dict = this.layout.fields_dict; | ||||
this.fields = this.layout.fields_list; | this.fields = this.layout.fields_list; | ||||
this.dashboard = new frappe.ui.form.Dashboard({ | |||||
frm: this, | |||||
parent: $('<div class="form-dashboard">').insertAfter(this.layout.wrapper.find('.form-message')) | |||||
}); | |||||
let dashboard_parent = $('<div class="form-dashboard">'); | |||||
let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper; | |||||
main_page.prepend(dashboard_parent); | |||||
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this); | |||||
this.tour = new frappe.ui.form.FormTour({ | this.tour = new frappe.ui.form.FormTour({ | ||||
frm: this | frm: this | ||||
@@ -181,8 +188,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
me.layout.refresh_dependency(); | me.layout.refresh_dependency(); | ||||
me.layout.refresh_sections(); | me.layout.refresh_sections(); | ||||
let object = me.script_manager.trigger(fieldname, doc.doctype, doc.name); | |||||
return object; | |||||
return me.script_manager.trigger(fieldname, doc.doctype, doc.name); | |||||
} | } | ||||
}); | }); | ||||
@@ -197,7 +203,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
if(doc.parent===me.docname && doc.parentfield===df.fieldname) { | if(doc.parent===me.docname && doc.parentfield===df.fieldname) { | ||||
me.dirty(); | me.dirty(); | ||||
me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc); | me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc); | ||||
me.script_manager.trigger(fieldname, doc.doctype, doc.name); | |||||
return me.script_manager.trigger(fieldname, doc.doctype, doc.name); | |||||
} | } | ||||
}); | }); | ||||
}); | }); | ||||
@@ -459,7 +465,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
}, | }, | ||||
() => this.cscript.is_onload && this.is_new() && this.focus_on_first_input(), | () => this.cscript.is_onload && this.is_new() && this.focus_on_first_input(), | ||||
() => this.run_after_load_hook(), | () => this.run_after_load_hook(), | ||||
() => this.dashboard.after_refresh() | |||||
() => this.dashboard.after_refresh(), | |||||
]); | ]); | ||||
} else { | } else { | ||||
@@ -468,6 +474,8 @@ frappe.ui.form.Form = class FrappeForm { | |||||
this.$wrapper.trigger('render_complete'); | this.$wrapper.trigger('render_complete'); | ||||
this.cscript.is_onload && this.set_first_tab_as_active(); | |||||
if(!this.hidden) { | if(!this.hidden) { | ||||
this.layout.show_empty_form_message(); | this.layout.show_empty_form_message(); | ||||
} | } | ||||
@@ -475,6 +483,11 @@ frappe.ui.form.Form = class FrappeForm { | |||||
this.scroll_to_element(); | this.scroll_to_element(); | ||||
} | } | ||||
set_first_tab_as_active() { | |||||
this.layout.tabs[0] | |||||
&& this.layout.tabs[0].set_active(); | |||||
} | |||||
focus_on_first_input() { | focus_on_first_input() { | ||||
let first = this.form_wrapper.find('.form-layout :input:visible:first'); | let first = this.form_wrapper.find('.form-layout :input:visible:first'); | ||||
if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { | if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { | ||||
@@ -1605,6 +1618,11 @@ frappe.ui.form.Form = class FrappeForm { | |||||
let $el = field.$wrapper; | let $el = field.$wrapper; | ||||
// set tab as active | |||||
if (field.tab && !field.tab.is_active()) { | |||||
field.tab.set_active(); | |||||
} | |||||
// uncollapse section | // uncollapse section | ||||
if (field.section.is_collapsed()) { | if (field.section.is_collapsed()) { | ||||
field.section.collapse(false); | field.section.collapse(false); | ||||
@@ -212,13 +212,12 @@ export default class Grid { | |||||
delete_all_rows() { | delete_all_rows() { | ||||
frappe.confirm(__("Are you sure you want to delete all rows?"), () => { | frappe.confirm(__("Are you sure you want to delete all rows?"), () => { | ||||
this.grid_rows.forEach(row => { | |||||
row.remove(); | |||||
}); | |||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); | |||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0); | |||||
this.frm.doc[this.df.fieldname] = []; | |||||
$(this.parent).find('.rows').empty(); | |||||
this.grid_rows = []; | |||||
this.refresh(); | this.refresh(); | ||||
this.frm && this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); | |||||
this.frm && this.frm.dirty(); | |||||
this.scroll_to_top(); | this.scroll_to_top(); | ||||
}); | }); | ||||
} | } | ||||
@@ -244,8 +243,10 @@ export default class Grid { | |||||
this.remove_rows_button.toggleClass('hidden', | this.remove_rows_button.toggleClass('hidden', | ||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | ||||
this.remove_all_rows_button.toggleClass('hidden', | |||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true); | |||||
let select_all_checkbox_checked = this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length; | |||||
let show_delete_all_btn = select_all_checkbox_checked && this.data.length > this.get_selected_children().length; | |||||
this.remove_all_rows_button.toggleClass('hidden', !show_delete_all_btn); | |||||
} | } | ||||
get_selected() { | get_selected() { | ||||
@@ -835,10 +836,11 @@ export default class Grid { | |||||
$.each(row, (ci, value) => { | $.each(row, (ci, value) => { | ||||
var fieldname = fieldnames[ci]; | var fieldname = fieldnames[ci]; | ||||
var df = frappe.meta.get_docfield(me.df.options, fieldname); | var df = frappe.meta.get_docfield(me.df.options, fieldname); | ||||
d[fieldnames[ci]] = value_formatter_map[df.fieldtype] | |||||
? value_formatter_map[df.fieldtype](value) | |||||
: value; | |||||
if (df) { | |||||
d[fieldnames[ci]] = value_formatter_map[df.fieldtype] | |||||
? value_formatter_map[df.fieldtype](value) | |||||
: value; | |||||
} | |||||
}); | }); | ||||
} | } | ||||
} | } | ||||
@@ -123,10 +123,12 @@ export default class GridRowForm { | |||||
.toggle(this.row.grid.is_editable()); | .toggle(this.row.grid.is_editable()); | ||||
} | } | ||||
refresh_field(fieldname) { | refresh_field(fieldname) { | ||||
if(this.fields_dict[fieldname]) { | |||||
this.fields_dict[fieldname].refresh(); | |||||
this.layout && this.layout.refresh_dependency(); | |||||
} | |||||
const field = this.fields_dict[fieldname]; | |||||
if (!field) return; | |||||
field.docname = this.row.doc.name; | |||||
field.refresh(); | |||||
this.layout && this.layout.refresh_dependency(); | |||||
} | } | ||||
set_focus() { | set_focus() { | ||||
// wait for animation and then focus on the first row | // wait for animation and then focus on the first row | ||||
@@ -1,27 +1,50 @@ | |||||
import '../class'; | |||||
import Section from "./section.js"; | |||||
import Tab from "./tab.js"; | |||||
import Column from "./column.js"; | |||||
frappe.ui.form.Layout = class Layout { | frappe.ui.form.Layout = class Layout { | ||||
constructor (opts) { | constructor (opts) { | ||||
this.views = {}; | this.views = {}; | ||||
this.pages = []; | this.pages = []; | ||||
this.tabs = []; | |||||
this.sections = []; | this.sections = []; | ||||
this.fields_list = []; | this.fields_list = []; | ||||
this.fields_dict = {}; | this.fields_dict = {}; | ||||
$.extend(this, opts); | $.extend(this, opts); | ||||
} | } | ||||
make() { | make() { | ||||
if (!this.parent && this.body) { | if (!this.parent && this.body) { | ||||
this.parent = this.body; | this.parent = this.body; | ||||
} | } | ||||
this.wrapper = $('<div class="form-layout">').appendTo(this.parent); | this.wrapper = $('<div class="form-layout">').appendTo(this.parent); | ||||
this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper); | this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper); | ||||
this.page = $('<div class="form-page"></div>').appendTo(this.wrapper); | |||||
if (!this.fields) { | if (!this.fields) { | ||||
this.fields = this.get_doctype_fields(); | this.fields = this.get_doctype_fields(); | ||||
} | } | ||||
this.setup_tabbing(); | |||||
if (this.is_tabbed_layout()) { | |||||
this.setup_tabbed_layout(); | |||||
} | |||||
this.setup_tab_events(); | |||||
this.render(); | this.render(); | ||||
} | } | ||||
setup_tabbed_layout() { | |||||
$(` | |||||
<div class="form-tabs-list"> | |||||
<ul class="nav form-tabs" id="form-tabs" role="tablist"></ul> | |||||
</div> | |||||
`).appendTo(this.page); | |||||
this.tabs_list = this.page.find('.form-tabs'); | |||||
this.tabs_content = $(`<div class="form-tab-content tab-content"></div>`).appendTo(this.page); | |||||
this.setup_events(); | |||||
} | |||||
show_empty_form_message() { | show_empty_form_message() { | ||||
if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { | if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { | ||||
this.show_message(__("This form does not have any input")); | this.show_message(__("This form does not have any input")); | ||||
@@ -87,49 +110,58 @@ frappe.ui.form.Layout = class Layout { | |||||
this.message.empty().addClass('hidden'); | this.message.empty().addClass('hidden'); | ||||
} | } | ||||
} | } | ||||
render (new_fields) { | |||||
var me = this; | |||||
var fields = new_fields || this.fields; | |||||
render(new_fields) { | |||||
let fields = new_fields || this.fields; | |||||
this.section = null; | this.section = null; | ||||
this.column = null; | this.column = null; | ||||
if (this.with_dashboard) { | |||||
this.setup_dashboard_section(); | |||||
if (this.no_opening_section() && !this.is_tabbed_layout()) { | |||||
this.fields.unshift({fieldtype: 'Section Break'}); | |||||
} | } | ||||
if (this.no_opening_section()) { | |||||
this.make_section(); | |||||
if (this.is_tabbed_layout()) { | |||||
let default_tab = {label: __('Details'), fieldname: 'details', fieldtype: "Tab Break"}; | |||||
let first_tab = this.fields[1].fieldtype === "Tab Break" ? this.fields[1] : null; | |||||
if (!first_tab) { | |||||
this.fields.splice(1, 0, default_tab); | |||||
} | |||||
} | } | ||||
$.each(fields, function (i, df) { | |||||
fields.forEach(df => { | |||||
switch (df.fieldtype) { | switch (df.fieldtype) { | ||||
case "Fold": | case "Fold": | ||||
me.make_page(df); | |||||
this.make_page(df); | |||||
break; | break; | ||||
case "Section Break": | case "Section Break": | ||||
me.make_section(df); | |||||
this.make_section(df); | |||||
break; | break; | ||||
case "Column Break": | case "Column Break": | ||||
me.make_column(df); | |||||
this.make_column(df); | |||||
break; | |||||
case "Tab Break": | |||||
this.make_tab(df); | |||||
break; | break; | ||||
default: | default: | ||||
me.make_field(df); | |||||
this.make_field(df); | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
no_opening_section () { | |||||
no_opening_section() { | |||||
return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length; | return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length; | ||||
} | } | ||||
setup_dashboard_section () { | |||||
if (this.no_opening_section()) { | |||||
this.fields.unshift({fieldtype: 'Section Break'}); | |||||
} | |||||
no_opening_tab() { | |||||
return (this.fields[1] && this.fields[1].fieldtype != "Tab Break") || !this.fields.length; | |||||
} | } | ||||
replace_field (fieldname, df, render) { | |||||
is_tabbed_layout() { | |||||
return this.fields.find(f => f.fieldtype === "Tab Break"); | |||||
} | |||||
replace_field(fieldname, df, render) { | |||||
df.fieldname = fieldname; // change of fieldname is avoided | df.fieldname = fieldname; // change of fieldname is avoided | ||||
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { | if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { | ||||
const fieldobj = this.init_field(df, render); | const fieldobj = this.init_field(df, render); | ||||
@@ -145,7 +177,7 @@ frappe.ui.form.Layout = class Layout { | |||||
} | } | ||||
} | } | ||||
make_field (df, colspan, render) { | |||||
make_field(df, colspan, render) { | |||||
!this.section && this.make_section(); | !this.section && this.make_section(); | ||||
!this.column && this.make_column(); | !this.column && this.make_column(); | ||||
@@ -159,9 +191,15 @@ frappe.ui.form.Layout = class Layout { | |||||
this.section.fields_list.push(fieldobj); | this.section.fields_list.push(fieldobj); | ||||
this.section.fields_dict[df.fieldname] = fieldobj; | this.section.fields_dict[df.fieldname] = fieldobj; | ||||
fieldobj.section = this.section; | fieldobj.section = this.section; | ||||
if (this.current_tab) { | |||||
fieldobj.tab = this.current_tab; | |||||
this.current_tab.fields_list.push(fieldobj); | |||||
this.current_tab.fields_dict[df.fieldname] = fieldobj; | |||||
} | |||||
} | } | ||||
init_field (df, render = false) { | |||||
init_field(df, render=false) { | |||||
const fieldobj = frappe.ui.form.make_control({ | const fieldobj = frappe.ui.form.make_control({ | ||||
df: df, | df: df, | ||||
doctype: this.doctype, | doctype: this.doctype, | ||||
@@ -176,8 +214,8 @@ frappe.ui.form.Layout = class Layout { | |||||
return fieldobj; | return fieldobj; | ||||
} | } | ||||
make_page (df) { // eslint-disable-line no-unused-vars | |||||
var me = this, | |||||
make_page(df) { // eslint-disable-line no-unused-vars | |||||
let me = this, | |||||
head = $('<div class="form-clickable-section text-center">\ | head = $('<div class="form-clickable-section text-center">\ | ||||
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\ | <a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\ | ||||
</div>').appendTo(this.wrapper); | </div>').appendTo(this.wrapper); | ||||
@@ -185,7 +223,7 @@ frappe.ui.form.Layout = class Layout { | |||||
this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper); | this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper); | ||||
this.fold_btn = head.find(".btn-fold").on("click", function () { | this.fold_btn = head.find(".btn-fold").on("click", function () { | ||||
var page = $(this).parent().next(); | |||||
let page = $(this).parent().next(); | |||||
if (page.hasClass("hide")) { | if (page.hasClass("hide")) { | ||||
$(this).removeClass("btn-fold").html(__("Hide details")); | $(this).removeClass("btn-fold").html(__("Hide details")); | ||||
page.removeClass("hide"); | page.removeClass("hide"); | ||||
@@ -202,12 +240,12 @@ frappe.ui.form.Layout = class Layout { | |||||
this.folded = true; | this.folded = true; | ||||
} | } | ||||
unfold () { | |||||
unfold() { | |||||
this.fold_btn.trigger('click'); | this.fold_btn.trigger('click'); | ||||
} | } | ||||
make_section (df) { | |||||
this.section = new frappe.ui.form.Section(this, df); | |||||
make_section(df) { | |||||
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout); | |||||
// append to layout fields | // append to layout fields | ||||
if (df) { | if (df) { | ||||
@@ -218,15 +256,23 @@ frappe.ui.form.Layout = class Layout { | |||||
this.column = null; | this.column = null; | ||||
} | } | ||||
make_column (df) { | |||||
this.column = new frappe.ui.form.Column(this.section, df); | |||||
make_column(df) { | |||||
this.column = new Column(this.section, df); | |||||
if (df && df.fieldname) { | if (df && df.fieldname) { | ||||
this.fields_list.push(this.column); | this.fields_list.push(this.column); | ||||
} | } | ||||
} | } | ||||
refresh (doc) { | |||||
var me = this; | |||||
make_tab(df) { | |||||
this.section = null; | |||||
let tab = new Tab(this, df, this.frm, this.tabs_list, this.tabs_content); | |||||
this.current_tab = tab; | |||||
this.make_section({fieldtype: 'Section Break'}); | |||||
this.tabs.push(tab); | |||||
return tab; | |||||
} | |||||
refresh(doc) { | |||||
if (doc) this.doc = doc; | if (doc) this.doc = doc; | ||||
if (this.frm) { | if (this.frm) { | ||||
@@ -234,7 +280,7 @@ frappe.ui.form.Layout = class Layout { | |||||
} | } | ||||
// NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called | // NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called | ||||
me.attach_doc_and_docfields(true); | |||||
this.attach_doc_and_docfields(true); | |||||
if (this.frm && this.frm.wrapper) { | if (this.frm && this.frm.wrapper) { | ||||
$(this.frm.wrapper).trigger("refresh-fields"); | $(this.frm.wrapper).trigger("refresh-fields"); | ||||
@@ -246,6 +292,9 @@ frappe.ui.form.Layout = class Layout { | |||||
// refresh sections | // refresh sections | ||||
this.refresh_sections(); | this.refresh_sections(); | ||||
// refresh tabs | |||||
this.tabbed_layout && this.refresh_tabs(); | |||||
if (this.frm) { | if (this.frm) { | ||||
// collapse sections | // collapse sections | ||||
this.refresh_section_collapse(); | this.refresh_section_collapse(); | ||||
@@ -277,7 +326,30 @@ frappe.ui.form.Layout = class Layout { | |||||
}); | }); | ||||
} | } | ||||
refresh_fields (fields) { | |||||
refresh_tabs() { | |||||
this.tabs.forEach(tab => { | |||||
if (!tab.wrapper.hasClass('hide') || !tab.parent.hasClass('hide')) { | |||||
tab.parent.removeClass('show hide'); | |||||
tab.wrapper.removeClass('show hide'); | |||||
if ( | |||||
tab.wrapper.find( | |||||
".form-section:not(.hide-control, .empty-section), .form-dashboard-section:not(.hide-control, .empty-section)" | |||||
).length | |||||
) { | |||||
tab.toggle(true); | |||||
} else { | |||||
tab.toggle(false); | |||||
} | |||||
} | |||||
}); | |||||
const visible_tabs = this.tabs.filter(tab => !tab.hidden); | |||||
if (visible_tabs && visible_tabs.length == 1) { | |||||
visible_tabs[0].parent.toggleClass('hide show'); | |||||
} | |||||
} | |||||
refresh_fields(fields) { | |||||
let fieldnames = fields.map((field) => { | let fieldnames = fields.map((field) => { | ||||
if (field.fieldname) return field.fieldname; | if (field.fieldname) return field.fieldname; | ||||
}); | }); | ||||
@@ -292,7 +364,7 @@ frappe.ui.form.Layout = class Layout { | |||||
}); | }); | ||||
} | } | ||||
add_fields (fields) { | |||||
add_fields(fields) { | |||||
this.render(fields); | this.render(fields); | ||||
this.refresh_fields(fields); | this.refresh_fields(fields); | ||||
} | } | ||||
@@ -300,11 +372,11 @@ frappe.ui.form.Layout = class Layout { | |||||
refresh_section_collapse () { | refresh_section_collapse () { | ||||
if (!(this.sections && this.sections.length)) return; | if (!(this.sections && this.sections.length)) return; | ||||
for (var i = 0; i < this.sections.length; i++) { | |||||
var section = this.sections[i]; | |||||
var df = section.df; | |||||
for (let i = 0; i < this.sections.length; i++) { | |||||
let section = this.sections[i]; | |||||
let df = section.df; | |||||
if (df && df.collapsible) { | if (df && df.collapsible) { | ||||
var collapse = true; | |||||
let collapse = true; | |||||
if (df.collapsible_depends_on) { | if (df.collapsible_depends_on) { | ||||
collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on); | collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on); | ||||
@@ -319,10 +391,10 @@ frappe.ui.form.Layout = class Layout { | |||||
} | } | ||||
} | } | ||||
attach_doc_and_docfields (refresh) { | |||||
var me = this; | |||||
for (var i = 0, l = this.fields_list.length; i < l; i++) { | |||||
var fieldobj = this.fields_list[i]; | |||||
attach_doc_and_docfields(refresh) { | |||||
let me = this; | |||||
for (let i = 0, l = this.fields_list.length; i < l; i++) { | |||||
let fieldobj = this.fields_list[i]; | |||||
if (me.doc) { | if (me.doc) { | ||||
fieldobj.doc = me.doc; | fieldobj.doc = me.doc; | ||||
fieldobj.doctype = me.doc.doctype; | fieldobj.doctype = me.doc.doctype; | ||||
@@ -339,41 +411,49 @@ frappe.ui.form.Layout = class Layout { | |||||
} | } | ||||
} | } | ||||
refresh_section_count () { | |||||
refresh_section_count() { | |||||
this.wrapper.find(".section-count-label:visible").each(function (i) { | this.wrapper.find(".section-count-label:visible").each(function (i) { | ||||
$(this).html(i + 1); | $(this).html(i + 1); | ||||
}); | }); | ||||
} | } | ||||
setup_tabbing () { | |||||
var me = this; | |||||
this.wrapper.on("keydown", function (ev) { | |||||
setup_events() { | |||||
this.tabs_list.off('click').on('click', '.nav-link', (e) => { | |||||
e.preventDefault(); | |||||
e.stopImmediatePropagation(); | |||||
$(e.currentTarget).tab('show'); | |||||
}); | |||||
} | |||||
setup_tab_events() { | |||||
this.wrapper.on("keydown", (ev) => { | |||||
if (ev.which == 9) { | if (ev.which == 9) { | ||||
var current = $(ev.target), | |||||
doctype = current.attr("data-doctype"), | |||||
fieldname = current.attr("data-fieldname"); | |||||
if (doctype) | |||||
return me.handle_tab(doctype, fieldname, ev.shiftKey); | |||||
let current = $(ev.target); | |||||
let doctype = current.attr("data-doctype"); | |||||
let fieldname = current.attr("data-fieldname"); | |||||
if (doctype) { | |||||
return this.handle_tab(doctype, fieldname, ev.shiftKey); | |||||
} | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
handle_tab (doctype, fieldname, shift) { | |||||
var me = this, | |||||
grid_row = null, | |||||
handle_tab(doctype, fieldname, shift) { | |||||
let grid_row = null, | |||||
prev = null, | prev = null, | ||||
fields = me.fields_list, | |||||
in_grid = false, | |||||
fields = this.fields_list, | |||||
focused = false; | focused = false; | ||||
// in grid | // in grid | ||||
if (doctype != me.doctype) { | |||||
grid_row = me.get_open_grid_row(); | |||||
if (doctype != this.doctype) { | |||||
grid_row = this.get_open_grid_row(); | |||||
if (!grid_row || !grid_row.layout) { | if (!grid_row || !grid_row.layout) { | ||||
return; | return; | ||||
} | } | ||||
fields = grid_row.layout.fields_list; | fields = grid_row.layout.fields_list; | ||||
} | } | ||||
for (var i = 0, len = fields.length; i < len; i++) { | |||||
for (let i = 0, len = fields.length; i < len; i++) { | |||||
if (fields[i].df.fieldname == fieldname) { | if (fields[i].df.fieldname == fieldname) { | ||||
if (shift) { | if (shift) { | ||||
if (prev) { | if (prev) { | ||||
@@ -384,7 +464,7 @@ frappe.ui.form.Layout = class Layout { | |||||
break; | break; | ||||
} | } | ||||
if (i < len - 1) { | if (i < len - 1) { | ||||
focused = me.focus_on_next_field(i, fields); | |||||
focused = this.focus_on_next_field(i, fields); | |||||
} | } | ||||
if (focused) { | if (focused) { | ||||
@@ -408,17 +488,19 @@ frappe.ui.form.Layout = class Layout { | |||||
// next row | // next row | ||||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | ||||
} | } | ||||
} else { | |||||
} else if (!shift) { | |||||
// End of tab navigation | |||||
$(this.primary_button).focus(); | $(this.primary_button).focus(); | ||||
} | } | ||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
focus_on_next_field (start_idx, fields) { | |||||
focus_on_next_field(start_idx, fields) { | |||||
// loop to find next eligible fields | // loop to find next eligible fields | ||||
for (var i = start_idx + 1, len = fields.length; i < len; i++) { | |||||
var field = fields[i]; | |||||
for (let i = start_idx + 1, len = fields.length; i < len; i++) { | |||||
let field = fields[i]; | |||||
if (this.is_visible(field)) { | if (this.is_visible(field)) { | ||||
if (field.df.fieldtype === "Table") { | if (field.df.fieldtype === "Table") { | ||||
// open table grid | // open table grid | ||||
@@ -437,10 +519,15 @@ frappe.ui.form.Layout = class Layout { | |||||
} | } | ||||
} | } | ||||
} | } | ||||
is_visible (field) { | |||||
return field.disp_status === "Write" && (field.$wrapper && field.$wrapper.is(":visible")); | |||||
is_visible(field) { | |||||
return field.disp_status === "Write" && (field.df && "hidden" in field.df && !field.df.hidden); | |||||
} | } | ||||
set_focus (field) { | |||||
set_focus(field) { | |||||
if (field.tab) { | |||||
field.tab.set_active(); | |||||
} | |||||
// next is table, show the table | // next is table, show the table | ||||
if (field.df.fieldtype=="Table") { | if (field.df.fieldtype=="Table") { | ||||
if (!field.grid.grid_rows.length) { | if (!field.grid.grid_rows.length) { | ||||
@@ -454,18 +541,19 @@ frappe.ui.form.Layout = class Layout { | |||||
field.$input.focus(); | field.$input.focus(); | ||||
} | } | ||||
} | } | ||||
get_open_grid_row () { | |||||
get_open_grid_row() { | |||||
return $(".grid-row-open").data("grid_row"); | return $(".grid-row-open").data("grid_row"); | ||||
} | } | ||||
refresh_dependency () { | |||||
refresh_dependency() { | |||||
// Resolve "depends_on" and show / hide accordingly | // Resolve "depends_on" and show / hide accordingly | ||||
var me = this; | |||||
// build dependants' dictionary | // build dependants' dictionary | ||||
var has_dep = false; | |||||
let has_dep = false; | |||||
for (var fkey in this.fields_list) { | |||||
var f = this.fields_list[fkey]; | |||||
for (let fkey in this.fields_list) { | |||||
let f = this.fields_list[fkey]; | |||||
f.dependencies_clear = true; | f.dependencies_clear = true; | ||||
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { | if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { | ||||
has_dep = true; | has_dep = true; | ||||
@@ -475,8 +563,8 @@ frappe.ui.form.Layout = class Layout { | |||||
if (!has_dep) return; | if (!has_dep) return; | ||||
// show / hide based on values | // show / hide based on values | ||||
for (var i = me.fields_list.length - 1; i >= 0; i--) { | |||||
var f = me.fields_list[i]; | |||||
for (let i = this.fields_list.length - 1; i >= 0; i--) { | |||||
let f = this.fields_list[i]; | |||||
f.guardian_has_value = true; | f.guardian_has_value = true; | ||||
if (f.df.depends_on) { | if (f.df.depends_on) { | ||||
// evaluate guardian | // evaluate guardian | ||||
@@ -508,7 +596,8 @@ frappe.ui.form.Layout = class Layout { | |||||
this.refresh_section_count(); | this.refresh_section_count(); | ||||
} | } | ||||
set_dependant_property (condition, fieldname, property) { | |||||
set_dependant_property(condition, fieldname, property) { | |||||
let set_property = this.evaluate_depends_on_value(condition); | let set_property = this.evaluate_depends_on_value(condition); | ||||
let value = set_property ? 1 : 0; | let value = set_property ? 1 : 0; | ||||
let form_obj; | let form_obj; | ||||
@@ -530,19 +619,20 @@ frappe.ui.form.Layout = class Layout { | |||||
} | } | ||||
} | } | ||||
} | } | ||||
evaluate_depends_on_value (expression) { | |||||
var out = null; | |||||
var doc = this.doc; | |||||
evaluate_depends_on_value(expression) { | |||||
let out = null; | |||||
let doc = this.doc; | |||||
if (!doc && this.get_values) { | if (!doc && this.get_values) { | ||||
var doc = this.get_values(true); | |||||
doc = this.get_values(true); | |||||
} | } | ||||
if (!doc) { | if (!doc) { | ||||
return; | return; | ||||
} | } | ||||
var parent = this.frm ? this.frm.doc : this.doc || null; | |||||
let parent = this.frm ? this.frm.doc : this.doc || null; | |||||
if (typeof (expression) === 'boolean') { | if (typeof (expression) === 'boolean') { | ||||
out = expression; | out = expression; | ||||
@@ -574,160 +664,3 @@ frappe.ui.form.Layout = class Layout { | |||||
return out; | return out; | ||||
} | } | ||||
}; | }; | ||||
frappe.ui.form.Section = class FormSection { | |||||
constructor(layout, df) { | |||||
this.layout = layout; | |||||
this.df = df || {}; | |||||
this.fields_list = []; | |||||
this.fields_dict = {}; | |||||
this.make(); | |||||
// if (this.frm) | |||||
// this.section.body.css({"padding":"0px 3%"}) | |||||
this.row = { | |||||
wrapper: this.wrapper | |||||
}; | |||||
this.refresh(); | |||||
} | |||||
make() { | |||||
if (!this.layout.page) { | |||||
this.layout.page = $('<div class="form-page"></div>').appendTo(this.layout.wrapper); | |||||
} | |||||
let make_card = this.layout.card_layout; | |||||
this.wrapper = $(`<div class="row form-section ${ make_card ? "card-section" : "" }">`) | |||||
.appendTo(this.layout.page); | |||||
this.layout.sections.push(this); | |||||
if (this.df) { | |||||
if (this.df.label) { | |||||
this.make_head(); | |||||
} | |||||
if (this.df.description) { | |||||
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>') | |||||
.appendTo(this.wrapper); | |||||
} | |||||
if (this.df.cssClass) { | |||||
this.wrapper.addClass(this.df.cssClass); | |||||
} | |||||
if (this.df.hide_border) { | |||||
this.wrapper.toggleClass("hide-border", true); | |||||
} | |||||
} | |||||
// for bc | |||||
this.body = $('<div class="section-body">').appendTo(this.wrapper); | |||||
} | |||||
make_head () { | |||||
this.head = $(`<div class="section-head"> | |||||
${__(this.df.label)} | |||||
<span class="ml-2 collapse-indicator mb-1"> | |||||
</span> | |||||
</div>`); | |||||
this.head.appendTo(this.wrapper); | |||||
this.indicator = this.head.find('.collapse-indicator'); | |||||
this.indicator.hide(); | |||||
if (this.df.collapsible) { | |||||
// show / hide based on status | |||||
this.collapse_link = this.head.on("click", () => { | |||||
this.collapse(); | |||||
}); | |||||
this.indicator.show(); | |||||
} | |||||
} | |||||
refresh() { | |||||
if (!this.df) | |||||
return; | |||||
// hide if explictly hidden | |||||
var hide = this.df.hidden || this.df.hidden_due_to_dependency; | |||||
// hide if no perm | |||||
if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { | |||||
hide = true; | |||||
} | |||||
this.wrapper.toggleClass("hide-control", !!hide); | |||||
} | |||||
collapse (hide) { | |||||
// unknown edge case | |||||
if (!(this.head && this.body)) { | |||||
return; | |||||
} | |||||
if (hide===undefined) { | |||||
hide = !this.body.hasClass("hide"); | |||||
} | |||||
this.body.toggleClass("hide", hide); | |||||
this.head.toggleClass("collapsed", hide); | |||||
let indicator_icon = hide ? 'down' : 'up-line'; | |||||
this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); | |||||
// refresh signature fields | |||||
this.fields_list.forEach((f) => { | |||||
if (f.df.fieldtype == 'Signature') { | |||||
f.refresh(); | |||||
} | |||||
}); | |||||
} | |||||
is_collapsed() { | |||||
return this.body.hasClass('hide'); | |||||
} | |||||
has_missing_mandatory () { | |||||
var missing_mandatory = false; | |||||
for (var j = 0, l = this.fields_list.length; j < l; j++) { | |||||
var section_df = this.fields_list[j].df; | |||||
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) { | |||||
missing_mandatory = true; | |||||
break; | |||||
} | |||||
} | |||||
return missing_mandatory; | |||||
} | |||||
}; | |||||
frappe.ui.form.Column = class FormColumn { | |||||
constructor(section, df) { | |||||
if (!df) df = {}; | |||||
this.df = df; | |||||
this.section = section; | |||||
this.make(); | |||||
this.resize_all_columns(); | |||||
} | |||||
make () { | |||||
this.wrapper = $('<div class="form-column">\ | |||||
<form>\ | |||||
</form>\ | |||||
</div>').appendTo(this.section.body) | |||||
.find("form") | |||||
.on("submit", function () { | |||||
return false; | |||||
}); | |||||
if (this.df.label) { | |||||
$('<label class="control-label">' + __(this.df.label) | |||||
+ '</label>').appendTo(this.wrapper); | |||||
} | |||||
} | |||||
resize_all_columns () { | |||||
// distribute all columns equally | |||||
var colspan = cint(12 / this.section.wrapper.find(".form-column").length); | |||||
this.section.wrapper.find(".form-column").removeClass() | |||||
.addClass("form-column") | |||||
.addClass("col-sm-" + colspan); | |||||
} | |||||
refresh () { | |||||
this.section.refresh(); | |||||
} | |||||
}; |
@@ -2,86 +2,191 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
constructor(opts) { | 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 */ | ||||
Object.assign(this, opts); | Object.assign(this, opts); | ||||
var me = this; | |||||
if (this.doctype != "[Select]") { | |||||
frappe.model.with_doctype(this.doctype, function () { | |||||
me.make(); | |||||
}); | |||||
this.for_select = this.doctype == "[Select]"; | |||||
if (!this.for_select) { | |||||
frappe.model.with_doctype(this.doctype, () => this.init()); | |||||
} else { | } else { | ||||
this.make(); | |||||
this.init(); | |||||
} | } | ||||
} | } | ||||
make() { | |||||
let me = this; | |||||
init() { | |||||
this.page_length = 20; | this.page_length = 20; | ||||
this.start = 0; | this.start = 0; | ||||
let fields = this.get_primary_filters(); | |||||
this.fields = this.get_fields(); | |||||
this.make(); | |||||
} | |||||
get_fields() { | |||||
const primary_fields = this.get_primary_filters(); | |||||
const result_fields = this.get_result_fields(); | |||||
const data_fields = this.get_data_fields(); | |||||
const child_selection_fields = this.get_child_selection_fields(); | |||||
// Make results area | |||||
fields = fields.concat([ | |||||
{ fieldtype: "HTML", fieldname: "results_area" }, | |||||
return [...primary_fields, ...result_fields, ...data_fields, ...child_selection_fields]; | |||||
} | |||||
get_result_fields() { | |||||
const show_next_page = () => { | |||||
this.start += 20; | |||||
this.get_results(); | |||||
}; | |||||
return [ | |||||
{ | { | ||||
fieldtype: "Button", fieldname: "more_btn", label: __("More"), | |||||
click: () => { | |||||
this.start += 20; | |||||
this.get_results(); | |||||
} | |||||
fieldtype: "HTML", fieldname: "results_area" | |||||
}, | |||||
{ | |||||
fieldtype: "Button", fieldname: "more_btn", | |||||
label: __("More"), click: show_next_page.bind(this) | |||||
} | } | ||||
]); | |||||
]; | |||||
} | |||||
// Custom Data Fields | |||||
if (this.data_fields) { | |||||
fields.push({ fieldtype: "Section Break" }); | |||||
fields = fields.concat(this.data_fields); | |||||
get_data_fields() { | |||||
if (this.data_fields && this.data_fields.length) { | |||||
// Custom Data Fields | |||||
return [ | |||||
{ fieldtype: "Section Break" }, | |||||
...this.data_fields | |||||
]; | |||||
} else { | |||||
return []; | |||||
} | } | ||||
} | |||||
get_child_selection_fields() { | |||||
const fields = []; | |||||
if (this.allow_child_item_selection && this.child_fieldname) { | |||||
fields.push({ fieldtype: "HTML", fieldname: "child_selection_area" }); | |||||
} | |||||
return fields; | |||||
} | |||||
make() { | |||||
let doctype_plural = this.doctype.plural(); | let doctype_plural = this.doctype.plural(); | ||||
let title = __("Select {0}", [this.for_select ? __("value") : __(doctype_plural)]); | |||||
this.dialog = new frappe.ui.Dialog({ | this.dialog = new frappe.ui.Dialog({ | ||||
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]), | |||||
fields: fields, | |||||
title: title, | |||||
fields: this.fields, | |||||
primary_action_label: this.primary_action_label || __("Get Items"), | primary_action_label: this.primary_action_label || __("Get Items"), | ||||
secondary_action_label: __("Make {0}", [__(me.doctype)]), | |||||
primary_action: function () { | |||||
let filters_data = me.get_custom_filters(); | |||||
me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data); | |||||
secondary_action_label: __("Make {0}", [__(this.doctype)]), | |||||
primary_action: () => { | |||||
let filters_data = this.get_custom_filters(); | |||||
const data_values = cur_dialog.get_values(); // to pass values of data fields | |||||
const filtered_children = this.get_selected_child_names(); | |||||
const selected_documents = [...this.get_checked_values(), ...this.get_parent_name_of_selected_children()]; | |||||
this.action(selected_documents, { | |||||
...this.args, | |||||
...data_values, | |||||
...filters_data, | |||||
filtered_children | |||||
}); | |||||
}, | }, | ||||
secondary_action: function (e) { | |||||
// If user wants to close the modal | |||||
if (e) { | |||||
frappe.route_options = {}; | |||||
if (Array.isArray(me.setters)) { | |||||
for (let df of me.setters) { | |||||
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; | |||||
} | |||||
} else { | |||||
Object.keys(me.setters).forEach(function (setter) { | |||||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; | |||||
}); | |||||
} | |||||
frappe.new_doc(me.doctype, true); | |||||
} | |||||
} | |||||
secondary_action: this.make_new_document.bind(this) | |||||
}); | }); | ||||
if (this.add_filters_group) { | if (this.add_filters_group) { | ||||
this.make_filter_area(); | this.make_filter_area(); | ||||
} | } | ||||
this.args = {}; | |||||
this.setup_results(); | |||||
this.bind_events(); | |||||
this.get_results(); | |||||
this.dialog.show(); | |||||
} | |||||
make_new_document(e) { | |||||
// If user wants to close the modal | |||||
if (e) { | |||||
this.set_route_options(); | |||||
frappe.new_doc(this.doctype, true); | |||||
} | |||||
} | |||||
set_route_options() { | |||||
// set route options to get pre-filled form fields | |||||
frappe.route_options = {}; | |||||
if (Array.isArray(this.setters)) { | |||||
for (let df of this.setters) { | |||||
frappe.route_options[df.fieldname] = this.dialog.fields_dict[df.fieldname].get_value() || undefined; | |||||
} | |||||
} else { | |||||
Object.keys(this.setters).forEach(setter => { | |||||
frappe.route_options[setter] = this.dialog.fields_dict[setter].get_value() || undefined; | |||||
}); | |||||
} | |||||
} | |||||
setup_results() { | |||||
this.$parent = $(this.dialog.body); | this.$parent = $(this.dialog.body); | ||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results" | |||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results mt-3" | |||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); | style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); | ||||
this.$results = this.$wrapper.find('.results'); | this.$results = this.$wrapper.find('.results'); | ||||
this.$results.append(this.make_list_row()); | this.$results.append(this.make_list_row()); | ||||
} | |||||
this.args = {}; | |||||
toggle_child_selection() { | |||||
if (this.dialog.fields_dict['allow_child_item_selection'].get_value()) { | |||||
this.get_child_result().then(r => { | |||||
this.child_results = r.message || []; | |||||
this.render_child_datatable(); | |||||
this.$wrapper.addClass('hidden'); | |||||
this.$child_wrapper.removeClass('hidden'); | |||||
this.dialog.fields_dict.more_btn.$wrapper.hide(); | |||||
}); | |||||
} else { | |||||
this.child_results = []; | |||||
this.get_results(); | |||||
this.$wrapper.removeClass('hidden'); | |||||
this.$child_wrapper.addClass('hidden'); | |||||
} | |||||
} | |||||
this.bind_events(); | |||||
this.get_results(); | |||||
this.dialog.show(); | |||||
render_child_datatable() { | |||||
if (!this.child_datatable) { | |||||
this.setup_child_datatable(); | |||||
} else { | |||||
setTimeout(() => { | |||||
this.child_datatable.rowmanager.checkMap = []; | |||||
this.child_datatable.refresh(this.get_child_datatable_rows()); | |||||
this.$child_wrapper.find('.dt-scrollable').css('height', '300px'); | |||||
}, 500); | |||||
} | |||||
} | |||||
get_child_datatable_columns() { | |||||
const parent = this.doctype; | |||||
return [parent, ...this.child_columns].map(d => ({ name: frappe.unscrub(d), editable: false })); | |||||
} | |||||
get_child_datatable_rows() { | |||||
return this.child_results.map(d => Object.values(d).slice(1)); // slice name field | |||||
} | |||||
setup_child_datatable() { | |||||
const header_columns = this.get_child_datatable_columns(); | |||||
const rows = this.get_child_datatable_rows(); | |||||
this.$child_wrapper = this.dialog.fields_dict.child_selection_area.$wrapper; | |||||
this.$child_wrapper.addClass('mt-3'); | |||||
this.child_datatable = new frappe.DataTable(this.$child_wrapper.get(0), { | |||||
columns: header_columns, | |||||
data: rows, | |||||
layout: 'fluid', | |||||
inlineFilters: true, | |||||
serialNoColumn: false, | |||||
checkboxColumn: true, | |||||
cellHeight: 35, | |||||
noDataMessage: __('No Data'), | |||||
disableReorderColumn: true | |||||
}); | |||||
this.$child_wrapper.find('.dt-scrollable').css('height', '300px'); | |||||
} | } | ||||
get_primary_filters() { | get_primary_filters() { | ||||
@@ -94,7 +199,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
columns[0] = [ | columns[0] = [ | ||||
{ | { | ||||
fieldtype: "Data", | fieldtype: "Data", | ||||
label: __("Search"), | |||||
label: __("Name"), | |||||
fieldname: "search_term" | fieldname: "search_term" | ||||
} | } | ||||
]; | ]; | ||||
@@ -127,6 +232,16 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
// now a is a fixed-size array with mutable entries | // now a is a fixed-size array with mutable entries | ||||
} | } | ||||
if (this.allow_child_item_selection) { | |||||
this.child_doctype = frappe.meta.get_docfield(this.doctype, this.child_fieldname).options; | |||||
columns[0].push({ | |||||
fieldtype: "Check", | |||||
label: __("Select {0}", [this.child_doctype]), | |||||
fieldname: "allow_child_item_selection", | |||||
onchange: this.toggle_child_selection.bind(this) | |||||
}); | |||||
} | |||||
fields = [ | fields = [ | ||||
...columns[0], | ...columns[0], | ||||
{ fieldtype: "Column Break" }, | { fieldtype: "Column Break" }, | ||||
@@ -156,6 +271,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
this.get_results(); | this.get_results(); | ||||
} | } | ||||
}); | }); | ||||
// 'Apply Filter' breaks since the filers are not in a popover | |||||
// Hence keeping it hidden | |||||
this.filter_group.wrapper.find('.apply-filters').hide(); | |||||
} | } | ||||
get_custom_filters() { | get_custom_filters() { | ||||
@@ -166,7 +284,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
}); | }); | ||||
}, {}); | }, {}); | ||||
} else { | } else { | ||||
return []; | |||||
return {}; | |||||
} | } | ||||
} | } | ||||
@@ -200,6 +318,34 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
}); | }); | ||||
} | } | ||||
get_parent_name_of_selected_children() { | |||||
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return []; | |||||
let parent_names = this.child_datatable.rowmanager.checkMap.reduce((parent_names, checked, index) => { | |||||
if (checked == 1) { | |||||
const parent_name = this.child_results[index].parent; | |||||
parent_names.push(parent_name); | |||||
} | |||||
return parent_names; | |||||
}, []); | |||||
return parent_names; | |||||
} | |||||
get_selected_child_names() { | |||||
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return []; | |||||
let checked_names = this.child_datatable.rowmanager.checkMap.reduce((checked_names, checked, index) => { | |||||
if (checked == 1) { | |||||
const child_row_name = this.child_results[index].name; | |||||
checked_names.push(child_row_name); | |||||
} | |||||
return checked_names; | |||||
}, []); | |||||
return checked_names; | |||||
} | |||||
get_checked_values() { | get_checked_values() { | ||||
// Return name of checked value. | // Return name of checked value. | ||||
return this.$results.find('.list-item-container').map(function () { | return this.$results.find('.list-item-container').map(function () { | ||||
@@ -276,6 +422,8 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
me.$results.append(me.make_list_row(result)); | me.$results.append(me.make_list_row(result)); | ||||
}); | }); | ||||
this.$results.find(".list-item--head").css("z-index", 0); | |||||
if (frappe.flags.auto_scroll) { | if (frappe.flags.auto_scroll) { | ||||
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); | this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); | ||||
} | } | ||||
@@ -297,7 +445,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
this.render_result_list(checked, 0, false); | this.render_result_list(checked, 0, false); | ||||
} | } | ||||
get_results() { | |||||
get_filters_from_setters() { | |||||
let me = this; | let me = this; | ||||
let filters = this.get_query ? this.get_query().filters : {} || {}; | let filters = this.get_query ? this.get_query().filters : {} || {}; | ||||
let filter_fields = []; | let filter_fields = []; | ||||
@@ -321,12 +469,18 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
}); | }); | ||||
} | } | ||||
let filter_group = this.get_custom_filters(); | |||||
Object.assign(filters, filter_group); | |||||
return [filters, filter_fields]; | |||||
} | |||||
get_args_for_search() { | |||||
let [filters, filter_fields] = this.get_filters_from_setters(); | |||||
let custom_filters = this.get_custom_filters(); | |||||
Object.assign(filters, custom_filters); | |||||
let args = { | |||||
doctype: me.doctype, | |||||
txt: me.dialog.fields_dict["search_term"].get_value(), | |||||
return { | |||||
doctype: this.doctype, | |||||
txt: this.dialog.fields_dict["search_term"].get_value(), | |||||
filters: filters, | filters: filters, | ||||
filter_fields: filter_fields, | filter_fields: filter_fields, | ||||
start: this.start, | start: this.start, | ||||
@@ -334,25 +488,81 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
query: this.get_query ? this.get_query().query : '', | query: this.get_query ? this.get_query().query : '', | ||||
as_dict: 1 | as_dict: 1 | ||||
}; | }; | ||||
frappe.call({ | |||||
} | |||||
async perform_search(args) { | |||||
const res = await frappe.call({ | |||||
type: "GET", | type: "GET", | ||||
method: 'frappe.desk.search.search_widget', | method: 'frappe.desk.search.search_widget', | ||||
no_spinner: true, | no_spinner: true, | ||||
args: args, | args: args, | ||||
callback: function (r) { | |||||
let more = 0; | |||||
me.results = []; | |||||
if (r.values.length) { | |||||
if (r.values.length > me.page_length) { | |||||
r.values.pop(); | |||||
more = 1; | |||||
} | |||||
r.values.forEach(function (result) { | |||||
result.checked = 0; | |||||
me.results.push(result); | |||||
}); | |||||
}); | |||||
const more = res.values.length && res.values.length > this.page_length ? 1 : 0; | |||||
if (more) { | |||||
res.values.pop(); | |||||
} | |||||
return [res, more]; | |||||
} | |||||
async get_results() { | |||||
const args = this.get_args_for_search(); | |||||
const [res, more] = await this.perform_search(args); | |||||
this.results = []; | |||||
if (res.values.length) { | |||||
res.values.forEach(result => { | |||||
result.checked = 0; | |||||
this.results.push(result); | |||||
}); | |||||
} | |||||
this.render_result_list(this.results, more); | |||||
} | |||||
async get_filtered_parents_for_child_search() { | |||||
const parent_search_args = this.get_args_for_search(); | |||||
parent_search_args.filter_fields = ['name']; | |||||
// eslint-disable-next-line no-unused-vars | |||||
const [response, _] = await this.perform_search(parent_search_args); | |||||
let parent_names = []; | |||||
if (response.values.length) { | |||||
parent_names = response.values.map(v => v.name); | |||||
} | |||||
return parent_names; | |||||
} | |||||
async add_parent_filters(filters) { | |||||
const parent_names = await this.get_filtered_parents_for_child_search(); | |||||
if (parent_names.length) { | |||||
filters.push([ "parent", "in", parent_names ]); | |||||
} | |||||
} | |||||
add_custom_child_filters(filters) { | |||||
if (this.add_filters_group && this.filter_group) { | |||||
this.filter_group.get_filters().forEach(filter => { | |||||
if (filter[0] == this.child_doctype) { | |||||
filters.push([filter[1], filter[2], filter[3]]); | |||||
} | } | ||||
me.render_result_list(me.results, more); | |||||
}); | |||||
} | |||||
} | |||||
async get_child_result() { | |||||
let filters = [["parentfield", "=", this.child_fieldname]]; | |||||
await this.add_parent_filters(filters); | |||||
this.add_custom_child_filters(filters); | |||||
return frappe.call({ | |||||
method: "frappe.client.get_list", | |||||
args: { | |||||
doctype: this.child_doctype, | |||||
filters: filters, | |||||
fields: ['name', 'parent', ...this.child_columns], | |||||
parent: this.doctype, | |||||
order_by: 'parent' | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
@@ -0,0 +1,146 @@ | |||||
export default class Section { | |||||
constructor(parent, df, card_layout) { | |||||
this.card_layout = card_layout; | |||||
this.parent = parent; | |||||
this.df = df || {}; | |||||
this.fields_list = []; | |||||
this.fields_dict = {}; | |||||
this.make(); | |||||
if (this.df.label && this.df.collapsible && localStorage.getItem(df.css_class + '-closed')) { | |||||
this.collapse(); | |||||
} | |||||
this.row = { | |||||
wrapper: this.wrapper | |||||
}; | |||||
this.refresh(); | |||||
} | |||||
make() { | |||||
let make_card = this.card_layout; | |||||
this.wrapper = $(`<div class="row | |||||
${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"} | |||||
${ make_card ? "card-section" : "" }"> | |||||
`).appendTo(this.parent); | |||||
if (this.df) { | |||||
if (this.df.label) { | |||||
this.make_head(); | |||||
} | |||||
if (this.df.description) { | |||||
this.description_wrapper = $( | |||||
`<div class="col-sm-12 form-section-description"> | |||||
${__(this.df.description)} | |||||
</div>` | |||||
); | |||||
this.wrapper.append(this.description_wrapper); | |||||
} | |||||
if (this.df.css_class) { | |||||
this.wrapper.addClass(this.df.css_class); | |||||
} | |||||
if (this.df.hide_border) { | |||||
this.wrapper.toggleClass("hide-border", true); | |||||
} | |||||
} | |||||
this.body = $('<div class="section-body">').appendTo(this.wrapper); | |||||
if (this.df.body_html) { | |||||
this.body.append(this.df.body_html); | |||||
} | |||||
} | |||||
make_head() { | |||||
this.head = $(` | |||||
<div class="section-head"> | |||||
${__(this.df.label)} | |||||
<span class="ml-2 collapse-indicator mb-1"></span> | |||||
</div> | |||||
`); | |||||
this.head.appendTo(this.wrapper); | |||||
this.indicator = this.head.find('.collapse-indicator'); | |||||
this.indicator.hide(); | |||||
if (this.df.collapsible) { | |||||
// show / hide based on status | |||||
this.collapse_link = this.head.on("click", () => { | |||||
this.collapse(); | |||||
}); | |||||
this.set_icon(); | |||||
this.indicator.show(); | |||||
} | |||||
} | |||||
refresh(hide) { | |||||
if (!this.df) return; | |||||
// hide if explicitly hidden | |||||
hide = hide || this.df.hidden || this.df.hidden_due_to_dependency; | |||||
this.wrapper.toggleClass("hide-control", !!hide); | |||||
} | |||||
collapse(hide) { | |||||
// unknown edge case | |||||
if (!(this.head && this.body)) { | |||||
return; | |||||
} | |||||
if (hide === undefined) { | |||||
hide = !this.body.hasClass("hide"); | |||||
} | |||||
this.body.toggleClass("hide", hide); | |||||
this.head && this.head.toggleClass("collapsed", hide); | |||||
this.set_icon(hide); | |||||
// refresh signature fields | |||||
this.fields_list.forEach((f) => { | |||||
if (f.df.fieldtype == 'Signature') { | |||||
f.refresh(); | |||||
} | |||||
}); | |||||
// save state for next reload ('' is falsy) | |||||
if (this.df.css_class) | |||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : ''); | |||||
} | |||||
set_icon(hide) { | |||||
let indicator_icon = hide ? 'down' : 'up-line'; | |||||
this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); | |||||
} | |||||
is_collapsed() { | |||||
return this.body.hasClass('hide'); | |||||
} | |||||
has_missing_mandatory () { | |||||
let missing_mandatory = false; | |||||
for (let j = 0, l = this.fields_list.length; j < l; j++) { | |||||
const section_df = this.fields_list[j].df; | |||||
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) { | |||||
missing_mandatory = true; | |||||
break; | |||||
} | |||||
} | |||||
return missing_mandatory; | |||||
} | |||||
hide() { | |||||
this.on_section_toggle(false); | |||||
} | |||||
show() { | |||||
this.on_section_toggle(true); | |||||
} | |||||
on_section_toggle(show) { | |||||
this.wrapper.toggleClass("hide-control", !show); | |||||
// this.on_section_toggle && this.on_section_toggle(show); | |||||
} | |||||
} |
@@ -0,0 +1,75 @@ | |||||
export default class Tab { | |||||
constructor(parent, df, frm, tabs_list, tabs_content) { | |||||
this.parent = parent; | |||||
this.df = df || {}; | |||||
this.frm = frm; | |||||
this.doctype = 'User'; | |||||
this.label = this.df && this.df.label; | |||||
this.tabs_list = tabs_list; | |||||
this.tabs_content = tabs_content; | |||||
this.fields_list = []; | |||||
this.fields_dict = {}; | |||||
this.make(); | |||||
this.refresh(); | |||||
} | |||||
make() { | |||||
const id = `${frappe.scrub(this.doctype, '-')}-${this.df.fieldname}`; | |||||
this.parent = $(` | |||||
<li class="nav-item"> | |||||
<a class="nav-link ${this.df.active ? "active": ""}" id="${id}-tab" | |||||
data-toggle="tab" | |||||
href="#${id}" | |||||
role="tab" | |||||
aria-controls="${this.label}"> | |||||
${__(this.label)} | |||||
</a> | |||||
</li> | |||||
`).appendTo(this.tabs_list); | |||||
this.wrapper = $(`<div class="tab-pane fade show ${this.df.active ? "active": ""}" | |||||
id="${id}" role="tabpanel" aria-labelledby="${id}-tab">`).appendTo(this.tabs_content); | |||||
} | |||||
refresh() { | |||||
if (!this.df) return; | |||||
// hide if explicitly hidden | |||||
let hide = this.df.hidden || this.df.hidden_due_to_dependency; | |||||
if (!hide && this.frm && !this.frm.get_perm(this.df.permlevel || 0, "read")) { | |||||
hide = true; | |||||
} | |||||
hide && this.toggle(false); | |||||
} | |||||
toggle(show) { | |||||
this.parent.toggleClass('hide', !show); | |||||
this.wrapper.toggleClass('hide', !show); | |||||
this.parent.toggleClass('show', show); | |||||
this.wrapper.toggleClass('show', show); | |||||
this.hidden = !show; | |||||
} | |||||
show() { | |||||
this.parent.show(); | |||||
} | |||||
hide() { | |||||
this.parent.hide(); | |||||
} | |||||
set_active() { | |||||
this.parent.find('.nav-link').tab('show'); | |||||
this.wrapper.addClass('show'); | |||||
} | |||||
is_active() { | |||||
return this.wrapper.hasClass('active'); | |||||
} | |||||
is_hidden() { | |||||
this.wrapper.hasClass('hide') | |||||
&& this.parent.hasClass('hide'); | |||||
} | |||||
} |
@@ -545,7 +545,7 @@ frappe.ui.form.Toolbar = class Toolbar { | |||||
show_jump_to_field_dialog() { | show_jump_to_field_dialog() { | ||||
let visible_fields_filter = f => | let visible_fields_filter = f => | ||||
!['Section Break', 'Column Break'].includes(f.df.fieldtype) | |||||
!['Section Break', 'Column Break', 'Tab Break'].includes(f.df.fieldtype) | |||||
&& !f.df.hidden | && !f.df.hidden | ||||
&& f.disp_status !== 'None'; | && f.disp_status !== 'None'; | ||||
@@ -6,7 +6,11 @@ frappe.views.BaseList = class BaseList { | |||||
} | } | ||||
show() { | show() { | ||||
frappe.run_serially([ | |||||
return frappe.run_serially([ | |||||
() => this.show_skeleton(), | |||||
() => this.fetch_meta(), | |||||
() => this.hide_skeleton(), | |||||
() => this.check_permissions(), | |||||
() => this.init(), | () => this.init(), | ||||
() => this.before_refresh(), | () => this.before_refresh(), | ||||
() => this.refresh(), | () => this.refresh(), | ||||
@@ -150,6 +154,22 @@ frappe.views.BaseList = class BaseList { | |||||
} | } | ||||
} | } | ||||
fetch_meta() { | |||||
return frappe.model.with_doctype(this.doctype); | |||||
} | |||||
show_skeleton() { | |||||
} | |||||
hide_skeleton() { | |||||
} | |||||
check_permissions() { | |||||
return true; | |||||
} | |||||
setup_page() { | setup_page() { | ||||
this.page = this.parent.page; | this.page = this.parent.page; | ||||
this.$page = $(this.parent); | this.$page = $(this.parent); | ||||
@@ -387,6 +407,14 @@ frappe.views.BaseList = class BaseList { | |||||
); | ); | ||||
} | } | ||||
get_group_by() { | |||||
let name_field = this.fields && this.fields.find(f => f[0] == 'name'); | |||||
if (name_field) { | |||||
return frappe.model.get_full_column_name(name_field[0], name_field[1]); | |||||
} | |||||
return null; | |||||
} | |||||
setup_view() { | setup_view() { | ||||
// for child classes | // for child classes | ||||
} | } | ||||
@@ -417,6 +445,7 @@ frappe.views.BaseList = class BaseList { | |||||
start: this.start, | start: this.start, | ||||
page_length: this.page_length, | page_length: this.page_length, | ||||
view: this.view, | view: this.view, | ||||
group_by: this.get_group_by() | |||||
}; | }; | ||||
} | } | ||||
@@ -463,8 +492,6 @@ frappe.views.BaseList = class BaseList { | |||||
} else { | } else { | ||||
this.data = this.data.concat(data); | this.data = this.data.concat(data); | ||||
} | } | ||||
this.data = this.data.uniqBy((d) => d.name); | |||||
} | } | ||||
freeze() { | freeze() { | ||||
@@ -9,35 +9,31 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||||
var me = this; | var me = this; | ||||
var doctype = route[1]; | var doctype = route[1]; | ||||
frappe.model.with_doctype(doctype, function () { | |||||
if (locals['DocType'][doctype].issingle) { | |||||
frappe.set_re_route('Form', doctype); | |||||
} else { | |||||
// List / Gantt / Kanban / etc | |||||
// File is a special view | |||||
const view_name = doctype !== 'File' ? frappe.utils.to_title_case(route[2] || 'List') : 'File'; | |||||
let view_class = frappe.views[view_name + 'View']; | |||||
if (!view_class) view_class = frappe.views.ListView; | |||||
// List / Gantt / Kanban / etc | |||||
// File is a special view | |||||
const view_name = doctype !== 'File' ? frappe.utils.to_title_case(route[2] || 'List') : 'File'; | |||||
let view_class = frappe.views[view_name + 'View']; | |||||
if (!view_class) view_class = frappe.views.ListView; | |||||
if (view_class && view_class.load_last_view && view_class.load_last_view()) { | |||||
// view can have custom routing logic | |||||
return; | |||||
} | |||||
if (view_class && view_class.load_last_view && view_class.load_last_view()) { | |||||
// view can have custom routing logic | |||||
return; | |||||
} | |||||
frappe.provide('frappe.views.list_view.' + doctype); | |||||
const page_name = frappe.get_route_str(); | |||||
if (!frappe.views.list_view[page_name]) { | |||||
frappe.views.list_view[page_name] = new view_class({ | |||||
doctype: doctype, | |||||
parent: me.make_page(true, page_name) | |||||
}); | |||||
} else { | |||||
frappe.container.change_to(page_name); | |||||
} | |||||
me.set_cur_list(); | |||||
frappe.provide('frappe.views.list_view.' + doctype); | |||||
const page_name = frappe.get_route_str(); | |||||
if (!frappe.views.list_view[page_name]) { | |||||
frappe.views.list_view[page_name] = new view_class({ | |||||
doctype: doctype, | |||||
parent: me.make_page(true, page_name) | |||||
}); | |||||
} else { | |||||
frappe.container.change_to(page_name); | |||||
} | |||||
me.set_cur_list(); | |||||
} | |||||
}); | |||||
} | } | ||||
show() { | show() { | ||||
@@ -33,14 +33,38 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
show() { | show() { | ||||
this.parent.disable_scroll_to_top = true; | this.parent.disable_scroll_to_top = true; | ||||
super.show(); | |||||
} | |||||
check_permissions() { | |||||
if (!this.has_permissions()) { | if (!this.has_permissions()) { | ||||
frappe.set_route(''); | frappe.set_route(''); | ||||
frappe.msgprint(__("Not permitted to view {0}", [this.doctype])); | |||||
return; | |||||
frappe.throw(__("Not permitted to view {0}", [this.doctype])); | |||||
} | } | ||||
} | |||||
super.show(); | |||||
show_skeleton() { | |||||
this.$list_skeleton = this.parent.page.container.find('.list-skeleton'); | |||||
if (!this.$list_skeleton.length) { | |||||
this.$list_skeleton = $(` | |||||
<div class="row list-skeleton"> | |||||
<div class="col-lg-2"> | |||||
<div class="list-skeleton-box"></div> | |||||
</div> | |||||
<div class="col"> | |||||
<div class="list-skeleton-box"></div> | |||||
</div> | |||||
</div> | |||||
`); | |||||
this.parent.page.container.find('.page-content').append(this.$list_skeleton); | |||||
} | |||||
this.parent.page.container.find('.layout-main').hide(); | |||||
this.$list_skeleton.show(); | |||||
} | |||||
hide_skeleton() { | |||||
this.$list_skeleton && this.$list_skeleton.hide(); | |||||
this.parent.page.container.find('.layout-main').show(); | |||||
} | } | ||||
get view_name() { | get view_name() { | ||||
@@ -548,6 +572,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
render() { | render() { | ||||
this.render_list(); | this.render_list(); | ||||
this.set_rows_as_checked(); | |||||
this.on_row_checked(); | this.on_row_checked(); | ||||
this.render_count(); | this.render_count(); | ||||
} | } | ||||
@@ -583,9 +608,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
const subject_field = this.columns[0].df; | const subject_field = this.columns[0].df; | ||||
let subject_html = ` | let subject_html = ` | ||||
<input class="level-item list-check-all hidden-xs" type="checkbox" | |||||
<input class="level-item list-check-all" type="checkbox" | |||||
title="${__("Select All")}"> | title="${__("Select All")}"> | ||||
<span class="level-item list-liked-by-me"> | |||||
<span class="level-item list-liked-by-me hidden-xs"> | |||||
<span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span> | <span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span> | ||||
</span> | </span> | ||||
<span class="level-item">${__(subject_field.label)}</span> | <span class="level-item">${__(subject_field.label)}</span> | ||||
@@ -622,7 +647,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
</div> | </div> | ||||
<div class="level-left checkbox-actions"> | <div class="level-left checkbox-actions"> | ||||
<div class="level list-subject"> | <div class="level list-subject"> | ||||
<input class="level-item list-check-all hidden-xs" type="checkbox" | |||||
<input class="level-item list-check-all" type="checkbox" | |||||
title="${__("Select All")}"> | title="${__("Select All")}"> | ||||
<span class="level-item list-header-meta"></span> | <span class="level-item list-header-meta"></span> | ||||
</div> | </div> | ||||
@@ -930,9 +955,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
let subject_html = ` | let subject_html = ` | ||||
<span class="level-item select-like"> | <span class="level-item select-like"> | ||||
<input class="list-row-checkbox hidden-xs" type="checkbox" | |||||
<input class="list-row-checkbox" type="checkbox" | |||||
data-name="${escape(doc.name)}"> | data-name="${escape(doc.name)}"> | ||||
<span class="list-row-like style="margin-bottom: 1px;"> | |||||
<span class="list-row-like hidden-xs style="margin-bottom: 1px;"> | |||||
${this.get_like_html(doc)} | ${this.get_like_html(doc)} | ||||
</span> | </span> | ||||
</span> | </span> | ||||
@@ -1139,6 +1164,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
if ( | if ( | ||||
$target.hasClass("filterable") || | $target.hasClass("filterable") || | ||||
$target.hasClass("select-like") || | $target.hasClass("select-like") || | ||||
$target.hasClass("file-select") || | |||||
$target.hasClass("list-row-like") || | $target.hasClass("list-row-like") || | ||||
$target.is(":checkbox") | $target.is(":checkbox") | ||||
) { | ) { | ||||
@@ -4,10 +4,10 @@ | |||||
frappe.provide('frappe.model'); | frappe.provide('frappe.model'); | ||||
$.extend(frappe.model, { | $.extend(frappe.model, { | ||||
no_value_type: ['Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', | |||||
no_value_type: ['Section Break', 'Column Break', 'Tab Break', 'HTML', 'Table', 'Table MultiSelect', | |||||
'Button', 'Image', 'Fold', 'Heading'], | 'Button', 'Image', 'Fold', 'Heading'], | ||||
layout_fields: ['Section Break', 'Column Break', 'Fold'], | |||||
layout_fields: ['Section Break', 'Column Break', 'Tab Break', 'Fold'], | |||||
std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by', | std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by', | ||||
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus', | '_user_tags', '_comments', '_assign', '_liked_by', 'docstatus', | ||||
@@ -131,6 +131,7 @@ $.extend(frappe.model, { | |||||
with_doctype: function(doctype, callback, async) { | with_doctype: function(doctype, callback, async) { | ||||
if(locals.DocType[doctype]) { | if(locals.DocType[doctype]) { | ||||
callback && callback(); | callback && callback(); | ||||
return Promise.resolve(); | |||||
} else { | } else { | ||||
let cached_timestamp = null; | let cached_timestamp = null; | ||||
let cached_doc = null; | let cached_doc = null; | ||||
@@ -464,31 +465,31 @@ $.extend(frappe.model, { | |||||
}, | }, | ||||
trigger: function(fieldname, value, doc) { | trigger: function(fieldname, value, doc) { | ||||
let tasks = []; | |||||
var runner = function(events, event_doc) { | |||||
$.each(events || [], function(i, fn) { | |||||
if(fn) { | |||||
let _promise = fn(fieldname, value, event_doc || doc); | |||||
const tasks = []; | |||||
function enqueue_events(events) { | |||||
if (!events) return; | |||||
for (const fn of events) { | |||||
if (!fn) continue; | |||||
tasks.push(() => { | |||||
const return_value = fn(fieldname, value, doc); | |||||
// if the trigger returns a promise, return it, | // if the trigger returns a promise, return it, | ||||
// or use the default promise frappe.after_ajax | // or use the default promise frappe.after_ajax | ||||
if (_promise && _promise.then) { | |||||
return _promise; | |||||
if (return_value && return_value.then) { | |||||
return return_value; | |||||
} else { | } else { | ||||
return frappe.after_server_call(); | return frappe.after_server_call(); | ||||
} | } | ||||
} | |||||
}); | |||||
}); | |||||
} | |||||
}; | }; | ||||
if(frappe.model.events[doc.doctype]) { | if(frappe.model.events[doc.doctype]) { | ||||
tasks.push(() => { | |||||
return runner(frappe.model.events[doc.doctype][fieldname]); | |||||
}); | |||||
tasks.push(() => { | |||||
return runner(frappe.model.events[doc.doctype]['*']); | |||||
}); | |||||
enqueue_events(frappe.model.events[doc.doctype][fieldname]); | |||||
enqueue_events(frappe.model.events[doc.doctype]['*']); | |||||
} | } | ||||
return frappe.run_serially(tasks); | return frappe.run_serially(tasks); | ||||
@@ -153,7 +153,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { | |||||
set_secondary_action(click) { | set_secondary_action(click) { | ||||
this.footer.removeClass('hide'); | this.footer.removeClass('hide'); | ||||
this.get_secondary_btn().removeClass('hide').on('click', click); | |||||
this.get_secondary_btn().removeClass('hide').off('click').on('click', click); | |||||
} | } | ||||
set_secondary_action_label(label) { | set_secondary_action_label(label) { | ||||