@@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then | |||
fi | |||
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 collation_server = 'utf8mb4_unicode_ci'"; | |||
@@ -58,4 +59,4 @@ cd ../.. | |||
bench start & | |||
bench --site test_site reinstall --yes | |||
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' | |||
uses: actions/setup-python@v2 | |||
with: | |||
python-version: 3.6 | |||
python-version: 3.7 | |||
- name: 'Clone repo' | |||
uses: actions/checkout@v2 | |||
@@ -9,7 +9,7 @@ concurrency: | |||
jobs: | |||
test: | |||
runs-on: ubuntu-18.04 | |||
runs-on: ubuntu-latest | |||
name: Patch Test | |||
@@ -18,7 +18,7 @@ jobs: | |||
node-version: 14 | |||
- uses: actions/setup-python@v2 | |||
with: | |||
python-version: '3.6' | |||
python-version: '3.7' | |||
- name: Set up bench and build assets | |||
run: | | |||
npm install -g yarn | |||
@@ -21,7 +21,7 @@ jobs: | |||
python-version: '12.x' | |||
- uses: actions/setup-python@v2 | |||
with: | |||
python-version: '3.6' | |||
python-version: '3.7' | |||
- name: Set up bench and build assets | |||
run: | | |||
npm install -g yarn | |||
@@ -13,7 +13,7 @@ concurrency: | |||
jobs: | |||
test: | |||
runs-on: ubuntu-18.04 | |||
runs-on: ubuntu-latest | |||
strategy: | |||
fail-fast: false | |||
@@ -127,4 +127,4 @@ jobs: | |||
name: MariaDB | |||
fail_ci_if_error: true | |||
files: /home/runner/frappe-bench/sites/coverage.xml | |||
verbose: true | |||
verbose: true |
@@ -12,7 +12,7 @@ concurrency: | |||
jobs: | |||
test: | |||
runs-on: ubuntu-18.04 | |||
runs-on: ubuntu-latest | |||
strategy: | |||
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: | |||
test: | |||
runs-on: ubuntu-18.04 | |||
runs-on: ubuntu-latest | |||
strategy: | |||
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.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 | |||
cy.get('[data-doctype="Contact"]').should('contain', '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.findByRole('button', {name: 'Save'}).click(); | |||
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 | |||
cy.get('[data-doctype="Contact"] > .count').should('contain', '1'); | |||
@@ -27,7 +30,7 @@ context('Dashboard links', () => { | |||
//Deleting the newly created 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.get('.actions-btn-group [data-label="Delete"]').click(); | |||
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 | |||
cy.wait(700); | |||
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'); | |||
}); | |||
@@ -71,7 +71,7 @@ context('Folder Navigation', () => { | |||
it('Deleting Test Folder from the home', () => { | |||
//Deleting the Test Folder added in the home directory | |||
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.get('.actions-btn-group [data-label="Delete"]').click(); | |||
cy.findByRole('button', {name: 'Yes'}).click(); | |||
@@ -8,7 +8,10 @@ context('Form', () => { | |||
}); | |||
it('create a new form', () => { | |||
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.get('.page-title').should('contain', 'Not Saved'); | |||
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"); | |||
}); | |||
}); | |||
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', () => { | |||
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | |||
cy.go_to_list('ToDo'); | |||
@@ -24,10 +41,11 @@ context('List View', () => { | |||
}).as('real-time-update'); | |||
cy.wrap(elements).contains('Approve').click(); | |||
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.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', () => { | |||
before(() => { | |||
cy.login(); | |||
cy.visit('/app/website'); | |||
}); | |||
it('Navigate to route with hash in document name', () => { | |||
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true}); | |||
@@ -11,4 +10,16 @@ context('Navigation', () => { | |||
cy.go('back'); | |||
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.click_listview_primary_button('Add ToDo'); | |||
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.wait(200); | |||
cy.findByRole('button', {name: 'Save'}).click(); | |||
@@ -5,14 +5,16 @@ context('Timeline Email', () => { | |||
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.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500}); | |||
cy.fill_field("description", "Test ToDo", "Text Editor"); | |||
cy.wait(500); | |||
cy.get('.primary-action').contains('Save').click({force: true}); | |||
cy.wait(700); | |||
}); | |||
it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { | |||
cy.visit('/app/todo'); | |||
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 > .dropdown-menu > li > .grey-link').eq(9).click(); | |||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click(); | |||
cy.visit('/app/todo'); | |||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); | |||
//Removing the added attachment | |||
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(); | |||
//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 | |||
user = local.conf.db_name | |||
password = local.conf.db_password | |||
port = local.conf.replica_db_port | |||
if local.conf.different_credentials_for_replica: | |||
user = local.conf.replica_db_name | |||
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 | |||
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 | |||
import os | |||
import re | |||
import json | |||
import shutil | |||
import subprocess | |||
from subprocess import getoutput | |||
from io import StringIO | |||
from tempfile import mkdtemp, mktemp | |||
from distutils.spawn import find_executable | |||
@@ -17,6 +18,8 @@ import psutil | |||
from urllib.parse import urlparse | |||
from simple_chalk import green | |||
from semantic_version import Version | |||
from requests import head | |||
from requests.exceptions import HTTPError | |||
timestamps = {} | |||
@@ -24,6 +27,12 @@ app_paths = None | |||
sites_path = os.path.abspath(os.getcwd()) | |||
class AssetsNotDownloadedError(Exception): | |||
pass | |||
class AssetsDontExistError(HTTPError): | |||
pass | |||
def download_file(url, prefix): | |||
from requests import get | |||
@@ -70,81 +79,94 @@ def build_missing_files(): | |||
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( | |||
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 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: | |||
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): | |||
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 | |||
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): | |||
"""Downloads and sets up Frappe assets if they exist based on the current | |||
commit HEAD. | |||
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") | |||
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: | |||
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: | |||
# 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): | |||
@@ -102,9 +102,24 @@ def get_commands(): | |||
from .site import commands as site_commands | |||
from .translate import commands as translate_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() |
@@ -3,7 +3,7 @@ import os | |||
import click | |||
import frappe | |||
from frappe.utils.rq import RedisQueue | |||
from frappe.utils.redis_queue import RedisQueue | |||
from frappe.installer import update_site_config | |||
@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 | |||
) | |||
site = get_site(context) | |||
frappe.init(site=site) | |||
force = context.force or force | |||
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 | |||
validate_database_sql(decompressed_file_name, _raise=not force) | |||
site = get_site(context) | |||
frappe.init(site=site) | |||
# dont allow downgrading to older versions of frappe without force | |||
if not force and is_downgrade(decompressed_file_name, verbose=True): | |||
warn_message = ( | |||
@@ -474,7 +474,7 @@ def remove_from_installed_apps(context, app): | |||
@click.command('uninstall-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('--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) | |||
@@ -738,6 +738,131 @@ def build_search_index(context): | |||
finally: | |||
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 = [ | |||
add_system_manager, | |||
backup, | |||
@@ -766,5 +891,7 @@ commands = [ | |||
add_to_hosts, | |||
start_ngrok, | |||
build_search_index, | |||
partial_restore | |||
partial_restore, | |||
trim_tables, | |||
trim_database, | |||
] |
@@ -408,20 +408,47 @@ def bulk_rename(context, doctype, path): | |||
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') | |||
@pass_context | |||
def mariadb(context): | |||
""" | |||
Enter into mariadb console for a given site. | |||
""" | |||
import os | |||
site = get_site(context) | |||
if not site: | |||
raise SiteNotSpecifiedError | |||
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') | |||
os.execv(mysql, [ | |||
mysql, | |||
@@ -434,15 +461,7 @@ def mariadb(context): | |||
"-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') | |||
subprocess.run([ psql, '-d', frappe.conf.db_name]) | |||
@@ -525,6 +544,74 @@ def console(context, autoreload=False): | |||
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.option('--app', help="For App") | |||
@click.option('--doctype', help="For DocType") | |||
@@ -814,6 +901,8 @@ commands = [ | |||
build, | |||
clear_cache, | |||
clear_website_cache, | |||
database, | |||
transform_database, | |||
jupyter, | |||
console, | |||
destroy_all_sessions, | |||
@@ -178,4 +178,4 @@ def set_link_title(doc): | |||
for link in doc.links: | |||
if not link.link_title: | |||
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): | |||
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 | |||
def has_common_link(self, doc): | |||
@@ -47,14 +47,14 @@ class Contact(Document): | |||
def get_link_for(self, link_doctype): | |||
'''Return the link name, if exists for the given link DocType''' | |||
for link in self.links: | |||
if link.link_doctype==link_doctype: | |||
if link.link_doctype == link_doctype: | |||
return link.link_name | |||
return None | |||
def has_link(self, doctype, name): | |||
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 | |||
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 | |||
import frappe | |||
from tenacity import retry, retry_if_exception_type, stop_after_attempt | |||
from frappe.model.document import Document | |||
@@ -10,25 +11,40 @@ class AccessLog(Document): | |||
@frappe.whitelist() | |||
@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 | |||
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) | |||
# `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() |
@@ -274,6 +274,8 @@ class DocType(Document): | |||
d.fieldname = d.fieldname + '_section' | |||
elif d.fieldtype=='Column Break': | |||
d.fieldname = d.fieldname + '_column' | |||
elif d.fieldtype=='Tab Break': | |||
d.fieldname = d.fieldname + '_tab' | |||
else: | |||
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) | |||
else: | |||
@@ -41,6 +41,7 @@ | |||
"fieldname": "counter", | |||
"fieldtype": "Int", | |||
"label": "Counter", | |||
"no_copy": 1, | |||
"read_only": 1 | |||
}, | |||
{ | |||
@@ -79,7 +80,7 @@ | |||
], | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-11-04 14:38:14.836056", | |||
"modified": "2021-09-13 20:07:47.617615", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"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": [ | |||
{ | |||
"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%" | |||
}, | |||
}, | |||
{ | |||
"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": [ | |||
{ | |||
"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 | |||
} | |||
], | |||
"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") | |||
@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): | |||
if user=="Administrator": | |||
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 | |||
# License: MIT. See LICENSE | |||
# License: MIT. See LICENSE | |||
import frappe, json | |||
from frappe.model.document import Document | |||
@@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]: | |||
fail_registry = queue.failed_job_registry | |||
for job_id in fail_registry.get_job_ids(): | |||
job = queue.fetch_job(job_id) | |||
add_job(job, queue.name) | |||
if job: | |||
add_job(job, queue.name) | |||
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: | |||
label = self.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) | |||
else: | |||
frappe.throw(_("Label is mandatory")) | |||
@@ -82,7 +82,7 @@ | |||
"label": "Type", | |||
"oldfieldname": "fieldtype", | |||
"oldfieldtype": "Select", | |||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\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, | |||
"search_index": 1 | |||
}, | |||
@@ -428,7 +428,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"istable": 1, | |||
"links": [], | |||
"modified": "2021-07-10 21:57:24.479749", | |||
"modified": "2021-07-11 21:57:24.479749", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Customize Form Field", | |||
@@ -34,7 +34,7 @@ class PropertySetter(Document): | |||
fields=['fieldname', 'label', 'fieldtype'], | |||
filters={ | |||
'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': ['!=', ''] | |||
}, | |||
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 | |||
if isinstance(value, (list, tuple)): | |||
# value is a tuple like ("!=", 0) | |||
_operator = value[0] | |||
_operator = value[0].lower() | |||
values[key] = value[1] | |||
if isinstance(value[1], (tuple, list)): | |||
# 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] | |||
def has_index(self, table_name, index_name): | |||
pass | |||
raise NotImplementedError | |||
def add_index(self, doctype, fields, index_name=None): | |||
pass | |||
raise NotImplementedError | |||
def add_unique(self, doctype, fields, constraint_name=None): | |||
pass | |||
raise NotImplementedError | |||
@staticmethod | |||
def get_index_name(fields): | |||
@@ -951,7 +951,7 @@ class Database(object): | |||
def escape(s, percent=True): | |||
"""Excape quotes and percent in given string.""" | |||
# implemented in specific class | |||
pass | |||
raise NotImplementedError | |||
@staticmethod | |||
def is_column_missing(e): | |||
@@ -22,11 +22,11 @@ class MariaDBDatabase(Database): | |||
def setup_type_map(self): | |||
self.db_type = 'mariadb' | |||
self.type_map = { | |||
'Currency': ('decimal', '18,6'), | |||
'Currency': ('decimal', '21,9'), | |||
'Int': ('int', '11'), | |||
'Long Int': ('bigint', '20'), | |||
'Float': ('decimal', '18,6'), | |||
'Percent': ('decimal', '18,6'), | |||
'Float': ('decimal', '21,9'), | |||
'Percent': ('decimal', '21,9'), | |||
'Check': ('int', '1'), | |||
'Small Text': ('text', ''), | |||
'Long Text': ('longtext', ''), | |||
@@ -51,7 +51,7 @@ class MariaDBDatabase(Database): | |||
'Color': ('varchar', self.VARCHAR_LEN), | |||
'Barcode': ('longtext', ''), | |||
'Geolocation': ('longtext', ''), | |||
'Duration': ('decimal', '18,6'), | |||
'Duration': ('decimal', '21,9'), | |||
'Icon': ('varchar', self.VARCHAR_LEN) | |||
} | |||
@@ -135,8 +135,8 @@ class MariaDBDatabase(Database): | |||
table_name = get_table_name(doctype) | |||
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") | |||
# exception types | |||
@@ -195,7 +195,7 @@ class MariaDBDatabase(Database): | |||
`password` TEXT NOT NULL, | |||
`encrypted` INT(1) NOT NULL DEFAULT 0, | |||
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): | |||
if not '__global_search' in self.get_tables(): | |||
@@ -72,7 +72,7 @@ CREATE TABLE `tabDocField` ( | |||
KEY `label` (`label`), | |||
KEY `fieldtype` (`fieldtype`), | |||
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, | |||
PRIMARY KEY (`name`), | |||
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` | |||
@@ -133,7 +133,7 @@ CREATE TABLE `tabDocType Action` ( | |||
PRIMARY KEY (`name`), | |||
KEY `parent` (`parent`), | |||
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` | |||
@@ -156,7 +156,7 @@ CREATE TABLE `tabDocType Link` ( | |||
PRIMARY KEY (`name`), | |||
KEY `parent` (`parent`), | |||
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` | |||
@@ -228,7 +228,7 @@ CREATE TABLE `tabDocType` ( | |||
`sender_field` varchar(255) DEFAULT NULL, | |||
PRIMARY KEY (`name`), | |||
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` | |||
@@ -239,7 +239,7 @@ CREATE TABLE `tabSeries` ( | |||
`name` varchar(100), | |||
`current` int(10) NOT NULL DEFAULT 0, | |||
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', | |||
`status` varchar(20) DEFAULT NULL, | |||
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, | |||
`value` text, | |||
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` | |||
@@ -283,7 +283,7 @@ CREATE TABLE `__Auth` ( | |||
`password` TEXT NOT NULL, | |||
`encrypted` INT(1) NOT NULL DEFAULT 0, | |||
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` | |||
@@ -311,7 +311,7 @@ CREATE TABLE `tabFile` ( | |||
KEY `parent` (`parent`), | |||
KEY `attached_to_name` (`attached_to_name`), | |||
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` | |||
@@ -334,4 +334,4 @@ CREATE TABLE `tabDefaultValue` ( | |||
PRIMARY KEY (`name`), | |||
KEY `parent` (`parent`), | |||
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): | |||
def create(self): | |||
add_text = '' | |||
additional_definitions = "" | |||
engine = self.meta.get("engine") or "InnoDB" | |||
varchar_len = frappe.db.VARCHAR_LEN | |||
# columns | |||
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_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 | |||
frappe.db.sql("""create table `%s` ( | |||
query = f"""create table `{self.table_name}` ( | |||
name varchar({varchar_len}) not null primary key, | |||
creation datetime(6), | |||
modified datetime(6), | |||
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable): | |||
parentfield varchar({varchar_len}), | |||
parenttype varchar({varchar_len}), | |||
idx int(8) not null default '0', | |||
%sindex parent(parent), | |||
{additional_definitions} | |||
index parent(parent), | |||
index modified(modified)) | |||
ENGINE={engine} | |||
ROW_FORMAT=COMPRESSED | |||
ROW_FORMAT=DYNAMIC | |||
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): | |||
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 | |||
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) | |||
dbman = DbManager(root_conn) | |||
dbman_kwargs = {} | |||
if no_mariadb_socket: | |||
dbman_kwargs["host"] = "%" | |||
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) | |||
else: | |||
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) | |||
dbman.create_database(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() | |||
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.extensions | |||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT | |||
from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION | |||
import frappe | |||
from frappe.database.database import Database | |||
@@ -31,11 +32,11 @@ class PostgresDatabase(Database): | |||
def setup_type_map(self): | |||
self.db_type = 'postgres' | |||
self.type_map = { | |||
'Currency': ('decimal', '18,6'), | |||
'Currency': ('decimal', '21,9'), | |||
'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), | |||
'Small Text': ('text', ''), | |||
'Long Text': ('text', ''), | |||
@@ -60,7 +61,7 @@ class PostgresDatabase(Database): | |||
'Color': ('varchar', self.VARCHAR_LEN), | |||
'Barcode': ('text', ''), | |||
'Geolocation': ('text', ''), | |||
'Duration': ('decimal', '18,6'), | |||
'Duration': ('decimal', '21,9'), | |||
'Icon': ('varchar', self.VARCHAR_LEN) | |||
} | |||
@@ -171,7 +172,7 @@ class PostgresDatabase(Database): | |||
@staticmethod | |||
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]: | |||
old_name = get_table_name(old_name) | |||
@@ -182,8 +183,8 @@ class PostgresDatabase(Database): | |||
table_name = get_table_name(doctype) | |||
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}') | |||
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 | |||
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: | |||
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'), | |||
ignore_inputs: true, | |||
}); | |||
frm.set_value("type", "Python"); | |||
}, | |||
refresh: function(frm) { | |||
frm.disable_save(); | |||
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, | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"execute_section", | |||
"type", | |||
"console", | |||
"commit", | |||
"output" | |||
"output", | |||
"sql_output", | |||
"database_processes_section", | |||
"show_processlist", | |||
"processlist" | |||
], | |||
"fields": [ | |||
{ | |||
@@ -40,13 +46,47 @@ | |||
"fieldname": "commit", | |||
"fieldtype": "Check", | |||
"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, | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2020-08-21 14:44:35.296877", | |||
"modified": "2021-09-15 17:17:44.844767", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "System Console", | |||
@@ -65,4 +105,4 @@ | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1 | |||
} | |||
} |
@@ -5,7 +5,7 @@ | |||
import json | |||
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 | |||
class SystemConsole(Document): | |||
@@ -13,8 +13,11 @@ class SystemConsole(Document): | |||
frappe.only_for('System Manager') | |||
try: | |||
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 | |||
self.output = frappe.get_traceback() | |||
@@ -33,4 +36,9 @@ class SystemConsole(Document): | |||
def execute_code(doc): | |||
console = frappe.get_doc(json.loads(doc)) | |||
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): | |||
""" | |||
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} | |||
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={ | |||
"document_type": doc.doctype, | |||
"document_name": doc.name | |||
}, 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() | |||
def get_documents_for_tag(tag): | |||
@@ -1,4 +1,5 @@ | |||
{ | |||
"actions": [], | |||
"creation": "2019-09-24 13:25:36.435685", | |||
"doctype": "DocType", | |||
"editable_grid": 1, | |||
@@ -44,7 +45,8 @@ | |||
"read_only": 1 | |||
} | |||
], | |||
"modified": "2019-10-03 16:42:35.932409", | |||
"links": [], | |||
"modified": "2021-09-20 16:53:37.217998", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Tag Link", | |||
@@ -61,6 +63,17 @@ | |||
"role": "System Manager", | |||
"share": 1, | |||
"write": 1 | |||
}, | |||
{ | |||
"create": 1, | |||
"email": 1, | |||
"export": 1, | |||
"print": 1, | |||
"read": 1, | |||
"report": 1, | |||
"role": "All", | |||
"share": 1, | |||
"write": 1 | |||
} | |||
], | |||
"read_only": 1, | |||
@@ -165,8 +165,6 @@ | |||
"default": "0", | |||
"fieldname": "is_standard", | |||
"fieldtype": "Check", | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
"label": "Is Standard", | |||
"search_index": 1 | |||
}, | |||
@@ -181,7 +179,6 @@ | |||
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user", | |||
"fieldname": "extends", | |||
"fieldtype": "Link", | |||
"in_standard_filter": 1, | |||
"label": "Extends", | |||
"options": "Workspace", | |||
"search_index": 1 | |||
@@ -228,6 +225,8 @@ | |||
"default": "0", | |||
"fieldname": "public", | |||
"fieldtype": "Check", | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
"label": "Public" | |||
}, | |||
{ | |||
@@ -265,11 +264,13 @@ | |||
"label": "Roles" | |||
} | |||
], | |||
"in_create": 1, | |||
"links": [], | |||
"modified": "2021-08-30 18:47:18.227154", | |||
"modified": "2021-09-16 12:01:06.450621", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Workspace", | |||
"naming_rule": "By fieldname", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
@@ -208,17 +208,17 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de | |||
if 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): | |||
for page in deleted_pages: | |||
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")): | |||
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): | |||
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) | |||
@@ -121,7 +121,7 @@ def validate_filters(data, filters): | |||
def setup_group_by(data): | |||
'''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'): | |||
frappe.throw(_('Invalid aggregate function')) | |||
@@ -226,7 +226,7 @@ | |||
}, | |||
{ | |||
"default": "UNSEEN", | |||
"depends_on": "eval: doc.enable_incoming", | |||
"depends_on": "eval: doc.enable_incoming && doc.use_imap", | |||
"fieldname": "email_sync_option", | |||
"fieldtype": "Select", | |||
"hide_days": 1, | |||
@@ -236,7 +236,7 @@ | |||
}, | |||
{ | |||
"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 ", | |||
"fieldname": "initial_sync_count", | |||
"fieldtype": "Select", | |||
@@ -567,7 +567,7 @@ | |||
"icon": "fa fa-inbox", | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2021-08-31 15:23:25.714366", | |||
"modified": "2021-09-21 16:44:25.728637", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Email Account", | |||
@@ -589,4 +589,4 @@ | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1 | |||
} | |||
} |
@@ -146,6 +146,7 @@ def get_context(context): | |||
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes: | |||
value = frappe.utils.cint(value) | |||
doc.reload() | |||
doc.set(fieldname, value) | |||
doc.flags.updater_reference = { | |||
'doctype': self.doctype, | |||
@@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase): | |||
notification.event = 'Value Change' | |||
notification.value_changed = 'status' | |||
notification.send_to_all_assignees = 1 | |||
notification.set_property_after_alert = 'description' | |||
notification.property_value = 'Changed by Notification' | |||
notification.save() | |||
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'): | |||
@@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase): | |||
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] | |||
self.assertTrue('test2@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) | |||
for entry in child_table: | |||
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): | |||
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) | |||
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.check_if_latest() | |||
@@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe" | |||
app_license = "MIT" | |||
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" | |||
@@ -164,7 +164,8 @@ doc_events = { | |||
"after_rename": "frappe.desk.notifications.clear_doctype_notifications", | |||
"on_cancel": [ | |||
"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": [ | |||
"frappe.desk.notifications.clear_doctype_notifications", | |||
@@ -445,9 +445,21 @@ def extract_sql_from_archive(sql_file_path): | |||
else: | |||
decompressed_file_name = sql_file_path | |||
# convert archive sql to latest compatible | |||
convert_archive_content(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): | |||
import subprocess | |||
@@ -457,7 +469,7 @@ def extract_sql_gzip(sql_gz_path): | |||
decompressed_file = original_file.rstrip(".gz") | |||
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file) | |||
subprocess.check_call(cmd, shell=True) | |||
except: | |||
except Exception: | |||
raise | |||
return decompressed_file | |||
@@ -41,6 +41,7 @@ data_fieldtypes = ( | |||
no_value_fields = ( | |||
'Section Break', | |||
'Column Break', | |||
'Tab Break', | |||
'HTML', | |||
'Table', | |||
'Table MultiSelect', | |||
@@ -53,6 +54,7 @@ no_value_fields = ( | |||
display_fieldtypes = ( | |||
'Section Break', | |||
'Column Break', | |||
'Tab Break', | |||
'HTML', | |||
'Button', | |||
'Image', | |||
@@ -307,7 +307,7 @@ class BaseDocument(object): | |||
doc["doctype"] = self.doctype | |||
for df in self.meta.get_table_fields(): | |||
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: | |||
for k in list(doc): | |||
@@ -4,6 +4,7 @@ | |||
from typing import List | |||
import frappe.defaults | |||
from frappe.query_builder.utils import Column | |||
import frappe.share | |||
from frappe import _ | |||
import frappe.permissions | |||
@@ -491,7 +492,7 @@ class DatabaseQuery(object): | |||
f.value = date_range | |||
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) | |||
fallback = "NULL" | |||
@@ -547,8 +548,12 @@ class DatabaseQuery(object): | |||
value = flt(f.value) | |||
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 | |||
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)}" | |||
if ( | |||
@@ -15,6 +15,7 @@ Example: | |||
''' | |||
from datetime import datetime | |||
import click | |||
import frappe, json, os | |||
from frappe.utils import cstr, cint, cast | |||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields | |||
@@ -658,27 +659,48 @@ def get_default_df(fieldname): | |||
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 | |||
as maintenance since removing a field in a DocType doesn't automatically | |||
delete the db field. | |||
""" | |||
ignore_fields = default_fields + optional_fields | |||
filters={ "issingle": 0 } | |||
UPDATED_TABLES = {} | |||
filters = {"issingle": 0} | |||
if 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 | |||
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) { | |||
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", | |||
"server_printer", | |||
"enable_print_server", | |||
"server_ip", | |||
"printer_name", | |||
"port", | |||
"raw_printing_section", | |||
"enable_raw_printing", | |||
"print_style_section", | |||
@@ -107,29 +104,11 @@ | |||
}, | |||
{ | |||
"default": "0", | |||
"depends_on": "enable_print_server", | |||
"fieldname": "enable_print_server", | |||
"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", | |||
@@ -183,7 +162,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2021-02-15 14:16:18.474254", | |||
"modified": "2021-09-17 12:59:14.783694", | |||
"modified_by": "Administrator", | |||
"module": "Printing", | |||
"name": "Print Settings", | |||
@@ -12,26 +12,6 @@ class PrintSettings(Document): | |||
def on_update(self): | |||
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() | |||
def is_print_server_enabled(): | |||
if not hasattr(frappe.local, 'enable_print_server'): | |||
@@ -165,10 +165,7 @@ frappe.ui.form.PrintView = class { | |||
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.printer_setting_dialog(); | |||
}); | |||
@@ -179,6 +176,12 @@ frappe.ui.form.PrintView = class { | |||
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) { | |||
@@ -460,72 +463,108 @@ frappe.ui.form.PrintView = class { | |||
printit() { | |||
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) { | |||
let w = window.open( | |||
frappe.urllib.get_full_url( | |||
@@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { | |||
} else if(f.fieldtype==="Column Break") { | |||
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) { | |||
if(!column) set_column(); | |||
@@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { | |||
init_visible_columns(f) { | |||
f.visible_columns = [] | |||
$.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) { | |||
// column names set as fieldname|width | |||
@@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { | |||
// add remaining fields | |||
$.each(doc_fields, function(j, f) { | |||
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); | |||
} | |||
}) | |||
@@ -4,7 +4,7 @@ | |||
</div> | |||
<div class="print-format-builder-sidebar-fields"> | |||
{% 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" | |||
data-fieldname="{%= f.fieldname %}"> | |||
<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/iconbar.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/form/link_selector.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 { | |||
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) { | |||
value = this.eval_expression(value); | |||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | |||
@@ -1,61 +1,65 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
import Section from "./section.js"; | |||
frappe.ui.form.Dashboard = class FormDashboard { | |||
constructor(opts) { | |||
$.extend(this, opts); | |||
constructor(parent, frm) { | |||
this.parent = parent; | |||
this.frm = frm; | |||
this.setup_dashboard_sections(); | |||
} | |||
setup_dashboard_sections() { | |||
this.progress_area = new Section(this.parent, { | |||
this.progress_area = this.make_section({ | |||
css_class: 'progress-area', | |||
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', | |||
hidden: 1, | |||
collapsible: 1, | |||
is_dashboard_section: 1, | |||
body_html: ` | |||
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></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', | |||
hidden: 1, | |||
collapsible: 1 | |||
is_dashboard_section: 1 | |||
}); | |||
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', | |||
hidden: 1, | |||
collapsible: 1, | |||
is_dashboard_section: 1, | |||
body_html: this.stats_area_row | |||
}); | |||
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', | |||
hidden: 1, | |||
collapsible: 1, | |||
is_dashboard_section: 1, | |||
body_html: this.transactions_area | |||
}); | |||
} | |||
make_section(df) { | |||
return new Section(this.parent, df); | |||
} | |||
reset() { | |||
this.hide(); | |||
// clear progress | |||
this.progress_area.body.empty(); | |||
this.progress_area.hide(); | |||
@@ -70,19 +74,19 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
// clear custom | |||
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 = { | |||
title, | |||
label, | |||
css_class, | |||
hidden, | |||
body_html, | |||
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) { | |||
@@ -154,7 +158,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
make_progress_chart(title) { | |||
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); | |||
return progress_chart; | |||
} | |||
@@ -169,7 +173,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
this.init_data(); | |||
} | |||
var show = false; | |||
let show = false; | |||
if (this.data && ((this.data.transactions || []).length | |||
|| (this.data.reports || []).length)) { | |||
@@ -197,11 +201,10 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
} | |||
after_refresh() { | |||
var me = this; | |||
// 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(); | |||
@@ -269,7 +272,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
} | |||
render_links() { | |||
var me = this; | |||
let me = this; | |||
this.links_area.show(); | |||
this.links_area.body.find('.btn-new').addClass('hidden'); | |||
if (this.data_rendered) { | |||
@@ -329,7 +332,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
open_document_list($link, show_open) { | |||
// show document list with filters | |||
var doctype = $link.attr('data-doctype'), | |||
let doctype = $link.attr('data-doctype'), | |||
names = $link.attr('data-names') || []; | |||
if (this.data.internal_links[doctype]) { | |||
@@ -351,8 +354,8 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
get_document_filter(doctype) { | |||
// return the default filter for the given document | |||
// 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.fieldname; | |||
@@ -371,7 +374,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
} | |||
// list all items from the transaction list | |||
var items = [], | |||
let items = [], | |||
me = this; | |||
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({ | |||
type: "GET", | |||
method: method, | |||
@@ -429,7 +432,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
} | |||
set_badge_count(doctype, open_count, count, names) { | |||
var $link = $(this.transactions_area) | |||
let $link = $(this.transactions_area) | |||
.find('.document-link[data-doctype="'+doctype+'"]'); | |||
if (open_count) { | |||
@@ -476,7 +479,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
this.heatmap_area.body.find('svg').css({'margin': 'auto'}); | |||
// 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) { | |||
heatmap_message.removeClass('hidden').html(this.data.heatmap_message); | |||
} else { | |||
@@ -491,9 +494,9 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
// 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) { | |||
colspan = 3; | |||
} else { | |||
@@ -505,7 +508,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
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); | |||
return indicator; | |||
@@ -513,9 +516,9 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
// graphs | |||
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, | |||
docname: this.frm.doc.name, | |||
}; | |||
@@ -579,11 +582,10 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
} | |||
add_comment(text, alert_class, permanent) { | |||
var me = this; | |||
this.set_headline_alert(text, alert_class); | |||
if (!permanent) { | |||
setTimeout(function() { | |||
me.clear_headline(); | |||
setTimeout(() => { | |||
this.clear_headline(); | |||
}, 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) { | |||
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); | |||
} | |||
@@ -353,7 +355,7 @@ class FormTimeline extends BaseTimeline { | |||
icon: 'branch', | |||
icon_size: 'sm', | |||
creation: workflow_log.creation, | |||
content: __(workflow_log.content), | |||
content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`, | |||
title: "Workflow", | |||
}); | |||
}); | |||
@@ -94,6 +94,11 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.watch_model_updates(); | |||
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({ | |||
frm: this, | |||
parent: $('<div>').appendTo(this.page.main.parent()) | |||
@@ -128,8 +133,8 @@ frappe.ui.form.Form = class FrappeForm { | |||
} | |||
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 | |||
this.meta.section_style='Simple'; // always simple! | |||
@@ -141,17 +146,19 @@ frappe.ui.form.Form = class FrappeForm { | |||
doctype_layout: this.doctype_layout, | |||
frm: this, | |||
with_dashboard: true, | |||
card_layout: true, | |||
card_layout: true | |||
}); | |||
this.layout.make(); | |||
this.fields_dict = this.layout.fields_dict; | |||
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({ | |||
frm: this | |||
@@ -181,8 +188,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
me.layout.refresh_dependency(); | |||
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) { | |||
me.dirty(); | |||
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.run_after_load_hook(), | |||
() => this.dashboard.after_refresh() | |||
() => this.dashboard.after_refresh(), | |||
]); | |||
} else { | |||
@@ -468,6 +474,8 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.$wrapper.trigger('render_complete'); | |||
this.cscript.is_onload && this.set_first_tab_as_active(); | |||
if(!this.hidden) { | |||
this.layout.show_empty_form_message(); | |||
} | |||
@@ -475,6 +483,11 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.scroll_to_element(); | |||
} | |||
set_first_tab_as_active() { | |||
this.layout.tabs[0] | |||
&& this.layout.tabs[0].set_active(); | |||
} | |||
focus_on_first_input() { | |||
let first = this.form_wrapper.find('.form-layout :input:visible:first'); | |||
if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { | |||
@@ -1605,6 +1618,11 @@ frappe.ui.form.Form = class FrappeForm { | |||
let $el = field.$wrapper; | |||
// set tab as active | |||
if (field.tab && !field.tab.is_active()) { | |||
field.tab.set_active(); | |||
} | |||
// uncollapse section | |||
if (field.section.is_collapsed()) { | |||
field.section.collapse(false); | |||
@@ -212,13 +212,12 @@ export default class Grid { | |||
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.frm && this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); | |||
this.frm && this.frm.dirty(); | |||
this.scroll_to_top(); | |||
}); | |||
} | |||
@@ -244,8 +243,10 @@ export default class Grid { | |||
this.remove_rows_button.toggleClass('hidden', | |||
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() { | |||
@@ -835,10 +836,11 @@ export default class Grid { | |||
$.each(row, (ci, value) => { | |||
var fieldname = fieldnames[ci]; | |||
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()); | |||
} | |||
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() { | |||
// 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 { | |||
constructor (opts) { | |||
this.views = {}; | |||
this.pages = []; | |||
this.tabs = []; | |||
this.sections = []; | |||
this.fields_list = []; | |||
this.fields_dict = {}; | |||
$.extend(this, opts); | |||
} | |||
make() { | |||
if (!this.parent && this.body) { | |||
this.parent = this.body; | |||
} | |||
this.wrapper = $('<div class="form-layout">').appendTo(this.parent); | |||
this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper); | |||
this.page = $('<div class="form-page"></div>').appendTo(this.wrapper); | |||
if (!this.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(); | |||
} | |||
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() { | |||
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")); | |||
@@ -87,49 +110,58 @@ frappe.ui.form.Layout = class Layout { | |||
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.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) { | |||
case "Fold": | |||
me.make_page(df); | |||
this.make_page(df); | |||
break; | |||
case "Section Break": | |||
me.make_section(df); | |||
this.make_section(df); | |||
break; | |||
case "Column Break": | |||
me.make_column(df); | |||
this.make_column(df); | |||
break; | |||
case "Tab Break": | |||
this.make_tab(df); | |||
break; | |||
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; | |||
} | |||
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 | |||
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { | |||
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.column && this.make_column(); | |||
@@ -159,9 +191,15 @@ frappe.ui.form.Layout = class Layout { | |||
this.section.fields_list.push(fieldobj); | |||
this.section.fields_dict[df.fieldname] = fieldobj; | |||
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({ | |||
df: df, | |||
doctype: this.doctype, | |||
@@ -176,8 +214,8 @@ frappe.ui.form.Layout = class Layout { | |||
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">\ | |||
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\ | |||
</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.fold_btn = head.find(".btn-fold").on("click", function () { | |||
var page = $(this).parent().next(); | |||
let page = $(this).parent().next(); | |||
if (page.hasClass("hide")) { | |||
$(this).removeClass("btn-fold").html(__("Hide details")); | |||
page.removeClass("hide"); | |||
@@ -202,12 +240,12 @@ frappe.ui.form.Layout = class Layout { | |||
this.folded = true; | |||
} | |||
unfold () { | |||
unfold() { | |||
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 | |||
if (df) { | |||
@@ -218,15 +256,23 @@ frappe.ui.form.Layout = class Layout { | |||
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) { | |||
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 (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 | |||
me.attach_doc_and_docfields(true); | |||
this.attach_doc_and_docfields(true); | |||
if (this.frm && this.frm.wrapper) { | |||
$(this.frm.wrapper).trigger("refresh-fields"); | |||
@@ -246,6 +292,9 @@ frappe.ui.form.Layout = class Layout { | |||
// refresh sections | |||
this.refresh_sections(); | |||
// refresh tabs | |||
this.tabbed_layout && this.refresh_tabs(); | |||
if (this.frm) { | |||
// collapse sections | |||
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) => { | |||
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.refresh_fields(fields); | |||
} | |||
@@ -300,11 +372,11 @@ frappe.ui.form.Layout = class Layout { | |||
refresh_section_collapse () { | |||
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) { | |||
var collapse = true; | |||
let collapse = true; | |||
if (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) { | |||
fieldobj.doc = me.doc; | |||
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).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) { | |||
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, | |||
fields = me.fields_list, | |||
in_grid = false, | |||
fields = this.fields_list, | |||
focused = false; | |||
// 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) { | |||
return; | |||
} | |||
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 (shift) { | |||
if (prev) { | |||
@@ -384,7 +464,7 @@ frappe.ui.form.Layout = class Layout { | |||
break; | |||
} | |||
if (i < len - 1) { | |||
focused = me.focus_on_next_field(i, fields); | |||
focused = this.focus_on_next_field(i, fields); | |||
} | |||
if (focused) { | |||
@@ -408,17 +488,19 @@ frappe.ui.form.Layout = class Layout { | |||
// next row | |||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | |||
} | |||
} else { | |||
} else if (!shift) { | |||
// End of tab navigation | |||
$(this.primary_button).focus(); | |||
} | |||
} | |||
return false; | |||
} | |||
focus_on_next_field (start_idx, fields) { | |||
focus_on_next_field(start_idx, 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 (field.df.fieldtype === "Table") { | |||
// 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 | |||
if (field.df.fieldtype=="Table") { | |||
if (!field.grid.grid_rows.length) { | |||
@@ -454,18 +541,19 @@ frappe.ui.form.Layout = class Layout { | |||
field.$input.focus(); | |||
} | |||
} | |||
get_open_grid_row () { | |||
get_open_grid_row() { | |||
return $(".grid-row-open").data("grid_row"); | |||
} | |||
refresh_dependency () { | |||
refresh_dependency() { | |||
// Resolve "depends_on" and show / hide accordingly | |||
var me = this; | |||
// 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; | |||
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { | |||
has_dep = true; | |||
@@ -475,8 +563,8 @@ frappe.ui.form.Layout = class Layout { | |||
if (!has_dep) return; | |||
// 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; | |||
if (f.df.depends_on) { | |||
// evaluate guardian | |||
@@ -508,7 +596,8 @@ frappe.ui.form.Layout = class Layout { | |||
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 value = set_property ? 1 : 0; | |||
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) { | |||
var doc = this.get_values(true); | |||
doc = this.get_values(true); | |||
} | |||
if (!doc) { | |||
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') { | |||
out = expression; | |||
@@ -574,160 +664,3 @@ frappe.ui.form.Layout = class Layout { | |||
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) { | |||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ | |||
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 { | |||
this.make(); | |||
this.init(); | |||
} | |||
} | |||
make() { | |||
let me = this; | |||
init() { | |||
this.page_length = 20; | |||
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 title = __("Select {0}", [this.for_select ? __("value") : __(doctype_plural)]); | |||
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"), | |||
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) { | |||
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.$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>`); | |||
this.$results = this.$wrapper.find('.results'); | |||
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() { | |||
@@ -94,7 +199,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||
columns[0] = [ | |||
{ | |||
fieldtype: "Data", | |||
label: __("Search"), | |||
label: __("Name"), | |||
fieldname: "search_term" | |||
} | |||
]; | |||
@@ -127,6 +232,16 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||
// 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 = [ | |||
...columns[0], | |||
{ fieldtype: "Column Break" }, | |||
@@ -156,6 +271,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||
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() { | |||
@@ -166,7 +284,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||
}); | |||
}, {}); | |||
} 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() { | |||
// Return name of checked value. | |||
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)); | |||
}); | |||
this.$results.find(".list-item--head").css("z-index", 0); | |||
if (frappe.flags.auto_scroll) { | |||
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); | |||
} | |||
get_results() { | |||
get_filters_from_setters() { | |||
let me = this; | |||
let filters = this.get_query ? this.get_query().filters : {} || {}; | |||
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, | |||
filter_fields: filter_fields, | |||
start: this.start, | |||
@@ -334,25 +488,81 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||
query: this.get_query ? this.get_query().query : '', | |||
as_dict: 1 | |||
}; | |||
frappe.call({ | |||
} | |||
async perform_search(args) { | |||
const res = await frappe.call({ | |||
type: "GET", | |||
method: 'frappe.desk.search.search_widget', | |||
no_spinner: true, | |||
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() { | |||
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.disp_status !== 'None'; | |||
@@ -6,7 +6,11 @@ frappe.views.BaseList = class BaseList { | |||
} | |||
show() { | |||
frappe.run_serially([ | |||
return frappe.run_serially([ | |||
() => this.show_skeleton(), | |||
() => this.fetch_meta(), | |||
() => this.hide_skeleton(), | |||
() => this.check_permissions(), | |||
() => this.init(), | |||
() => this.before_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() { | |||
this.page = this.parent.page; | |||
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() { | |||
// for child classes | |||
} | |||
@@ -417,6 +445,7 @@ frappe.views.BaseList = class BaseList { | |||
start: this.start, | |||
page_length: this.page_length, | |||
view: this.view, | |||
group_by: this.get_group_by() | |||
}; | |||
} | |||
@@ -463,8 +492,6 @@ frappe.views.BaseList = class BaseList { | |||
} else { | |||
this.data = this.data.concat(data); | |||
} | |||
this.data = this.data.uniqBy((d) => d.name); | |||
} | |||
freeze() { | |||
@@ -9,35 +9,31 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||
var me = this; | |||
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() { | |||
@@ -33,14 +33,38 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
show() { | |||
this.parent.disable_scroll_to_top = true; | |||
super.show(); | |||
} | |||
check_permissions() { | |||
if (!this.has_permissions()) { | |||
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() { | |||
@@ -548,6 +572,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
render() { | |||
this.render_list(); | |||
this.set_rows_as_checked(); | |||
this.on_row_checked(); | |||
this.render_count(); | |||
} | |||
@@ -583,9 +608,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const subject_field = this.columns[0].df; | |||
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")}"> | |||
<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> | |||
<span class="level-item">${__(subject_field.label)}</span> | |||
@@ -622,7 +647,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
</div> | |||
<div class="level-left checkbox-actions"> | |||
<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")}"> | |||
<span class="level-item list-header-meta"></span> | |||
</div> | |||
@@ -930,9 +955,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
let subject_html = ` | |||
<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)}"> | |||
<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)} | |||
</span> | |||
</span> | |||
@@ -1139,6 +1164,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if ( | |||
$target.hasClass("filterable") || | |||
$target.hasClass("select-like") || | |||
$target.hasClass("file-select") || | |||
$target.hasClass("list-row-like") || | |||
$target.is(":checkbox") | |||
) { | |||
@@ -4,10 +4,10 @@ | |||
frappe.provide('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'], | |||
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', | |||
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus', | |||
@@ -131,6 +131,7 @@ $.extend(frappe.model, { | |||
with_doctype: function(doctype, callback, async) { | |||
if(locals.DocType[doctype]) { | |||
callback && callback(); | |||
return Promise.resolve(); | |||
} else { | |||
let cached_timestamp = null; | |||
let cached_doc = null; | |||
@@ -464,31 +465,31 @@ $.extend(frappe.model, { | |||
}, | |||
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, | |||
// or use the default promise frappe.after_ajax | |||
if (_promise && _promise.then) { | |||
return _promise; | |||
if (return_value && return_value.then) { | |||
return return_value; | |||
} else { | |||
return frappe.after_server_call(); | |||
} | |||
} | |||
}); | |||
}); | |||
} | |||
}; | |||
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); | |||
@@ -153,7 +153,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { | |||
set_secondary_action(click) { | |||
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) { | |||