Merge branch 'frappe:develop' into flaky-ui-tests

This commit is contained in:
Shariq Ansari 2021-09-29 12:50:53 +05:30 committed by GitHub
commit 83f7baa2fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 2524 additions and 2342 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
};

View file

@ -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");
});
});

View file

@ -6,6 +6,28 @@ 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.get('.modal-body .frappe-control[data-fieldname="value"] input:visible').first().focus();
cy.get('.datepickers-container .datepicker.active').should('be.visible');
cy.get('.datepickers-container .datepicker.active .datepicker--cell-day.-current-').click({force: true});
cy.get('.modal-body .frappe-control[data-fieldname="value"] input:visible').first().focus();
cy.get('.datepickers-container .datepicker.active .datepicker--cell-day.-current-').click({force: true});
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');
@ -30,4 +52,3 @@ context('List View', () => {
});
});
});

View file

@ -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

View file

@ -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
pass
return assets_setup
return False
def symlink(target, link_name, overwrite=False):

View file

@ -102,7 +102,7 @@ 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"

View file

@ -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')

View file

@ -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

View file

@ -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()

File diff suppressed because it is too large Load diff

View file

@ -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:

View file

@ -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
}

View file

@ -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'

View file

@ -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

View file

@ -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
}

View file

@ -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"))

View file

@ -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",

View file

@ -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',

View file

@ -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"))

View file

@ -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)
}

View file

@ -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))

View file

@ -32,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', ''),
@ -61,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)
}

View file

@ -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'

View file

@ -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
}

View file

@ -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)
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)
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
})
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):

View file

@ -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,

View file

@ -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'))

View file

@ -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
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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)

View 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',

View file

@ -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 (

View file

@ -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));
}
});
}
}
});

View file

@ -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
}

View file

@ -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')

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestNetworkPrinterSettings(unittest.TestCase):
pass

View file

@ -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)
});
}
}
});

View file

@ -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",

View file

@ -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'):

View file

@ -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(

View file

@ -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);
}
})

View file

@ -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

View file

@ -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";

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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;
}
});
}
}

View file

@ -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

View file

@ -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();
}
};

View file

@ -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);
}
}

View file

@ -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');
}
}

View file

@ -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';

View file

@ -407,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
}
@ -437,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()
};
}
@ -483,8 +492,6 @@ frappe.views.BaseList = class BaseList {
} else {
this.data = this.data.concat(data);
}
this.data = this.data.uniqBy((d) => d.name);
}
freeze() {

View file

@ -572,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();
}
@ -607,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>
@ -646,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>
@ -954,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>

View file

@ -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',
@ -465,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);

View file

@ -5,7 +5,6 @@ frappe.provide('frappe.ui');
frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
constructor(opts) {
super(opts);
this.first_button = false;
this.dirty = false;
$.each(this.fields || [], function(i, f) {
if(!f.fieldname && f.label) {
@ -16,6 +15,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
this.set_values(this.values);
}
}
make() {
var me = this;
if(this.fields) {
@ -63,6 +63,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
});
}
catch_enter_as_submit() {
var me = this;
$(this.body).find('input[type="text"], input[type="password"], select').keypress(function(e) {
@ -74,13 +75,16 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
});
}
get_input(fieldname) {
var field = this.fields_dict[fieldname];
return $(field.txt ? field.txt : field.input);
}
get_field(fieldname) {
return this.fields_dict[fieldname];
}
get_values(ignore_errors) {
var ret = {};
var errors = [];
@ -113,14 +117,16 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
return ret;
}
get_value(key) {
var f = this.fields_dict[key];
return f && (f.get_value ? f.get_value() : null);
}
set_value(key, val){
set_value(key, val) {
return new Promise(resolve => {
var f = this.fields_dict[key];
if(f) {
if (f) {
f.set_value(val).then(() => {
f.set_input(val);
this.refresh_dependency();
@ -131,9 +137,11 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
});
}
set_input(key, val) {
return this.set_value(key, val);
}
set_values(dict) {
let promises = [];
for(var key in dict) {
@ -144,6 +152,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
return Promise.all(promises);
}
clear() {
for(var key in this.fields_dict) {
var f = this.fields_dict[key];
@ -152,8 +161,9 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
}
}
set_df_property (fieldname, prop, value) {
const field = this.get_field(fieldname);
const field = this.get_field(fieldname);
field.df[prop] = value;
field.refresh();
}

View file

@ -927,7 +927,16 @@ Object.assign(frappe.utils, {
// decodes base64 to string
let parts = dataURI.split(',');
const encoded_data = parts[1];
return decodeURIComponent(escape(atob(encoded_data)));
let decoded = atob(encoded_data);
try {
const escaped = escape(decoded);
decoded = decodeURIComponent(escaped);
} catch (e) {
// pass decodeURIComponent failure
// just return atob response
}
return decoded;
},
copy_to_clipboard(string) {
let input = $("<input>");

View file

@ -51,11 +51,11 @@
@extend .frappe-card;
}
.form-dashboard {
.form-dashboard-section {
.section-body:first-child {
margin-top: 0;
}
.form-dashboard-section .section-body {
.section-body {
display: block;
padding-left: var(--padding-md);
padding-right: var(--padding-md);
@ -303,6 +303,28 @@
}
}
.form-tabs-list {
margin-bottom: var(--margin-lg);
.form-tabs {
.nav-item {
.nav-link {
padding-bottom: var(--padding-md);
color: var(--gray-700);
padding-left: 0;
padding-right: 0;
margin-right: var(--margin-xl);
&.active {
font-weight: 500;
border-bottom: 1px solid var(--primary);
color: var(--text-color);
}
}
}
}
}
.progress-area {
padding-top: var(--padding-md);
padding-bottom: var(--padding-md);
@ -356,7 +378,4 @@
.form-column:not(:first-child) {
padding-top: var(--padding-md);
}
}

View file

@ -202,8 +202,12 @@ body {
}
// listviews
.list-row {
padding: 13px 15px !important;
.select-like {
margin-right: unset !important;
}
.list-count {
display: contents;
}
.doclist-row {

View file

@ -1,2 +1,2 @@
from pypika import *
from frappe.query_builder.utils import get_query_builder, patch_query_execute
from frappe.query_builder.utils import Column, get_query_builder, patch_query_execute

View file

@ -3,8 +3,10 @@ from typing import Any, Callable, Dict, get_type_hints
from importlib import import_module
from pypika import Query
from pypika.queries import Column
import frappe
from .builder import MariaDB, Postgres

View file

@ -82,19 +82,21 @@ class RateLimiter:
if self.rejected:
return Response(_("Too Many Requests"), status=429)
def rate_limit(key: str, limit: Union[int, Callable] = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'):
def rate_limit(key: str = None, limit: Union[int, Callable] = 5, seconds: int = 24*60*60, methods: Union[str, list] = 'ALL', ip_based: bool = True):
"""Decorator to rate limit an endpoint.
This will limit Number of requests per endpoint to `limit` within `seconds`.
Uses redis cache to track request counts.
:param key: Key is used to identify the requests uniqueness
:param key: Key is used to identify the requests uniqueness (Optional)
:param limit: Maximum number of requests to allow with in window time
:type limit: Callable or Integer
:param seconds: window time to allow requests
:param methods: Limit the validation for these methods.
`ALL` is a wildcard that applies rate limit on all methods.
:type methods: string or list or tuple
:param ip_based: flag to allow ip based rate-limiting
:type ip_based: Boolean
:returns: a decorator function that limit the number of requests per endpoint
"""
@ -102,17 +104,30 @@ def rate_limit(key: str, limit: Union[int, Callable] = 5, seconds: int= 24*60*60
@wraps(fun)
def wrapper(*args, **kwargs):
# Do not apply rate limits if method is not opted to check
if methods != 'ALL' and frappe.request.method.upper() not in methods:
if methods != 'ALL' and frappe.request and frappe.request.method and frappe.request.method.upper() not in methods:
return frappe.call(fun, **frappe.form_dict or kwargs)
_limit = limit() if callable(limit) else limit
identity = frappe.form_dict[key]
ip = frappe.local.request_ip if ip_based is True else None
user_key = frappe.form_dict[key] if key else None
identity = None
if key and ip_based:
identity = ":".join([ip, user_key])
identity = identity or ip or user_key
if not identity:
frappe.throw(_('Either key or IP flag is required.'))
cache_key = f"rl:{frappe.form_dict.cmd}:{identity}"
value = frappe.cache().get_value(cache_key, expires=True) or 0
value = frappe.cache().get(cache_key) or 0
if not value:
frappe.cache().set_value(cache_key, 0, expires_in_sec=seconds)
frappe.cache().setex(cache_key, seconds, 0)
value = frappe.cache().incrby(cache_key, 1)
if value > _limit:

View file

@ -1,31 +1,73 @@
.hero-and-content {
background-color: #f5f7fa;
background-color: var(--bg-color);
}
body {
background-color: var(--bg-color);
}
.page-card {
max-width: 360px;
padding: 15px;
margin: 70px auto;
border: 1px solid #d1d8dd;
border-radius: 4px;
background-color: #fff;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
background-color: var(--fg-color);
box-shadow: var(--shadow-base);
}
.for-reset-password {
margin: 80px 0;
}
.for-reset-password .page-card {
border: 0;
max-width: 450px;
margin: auto;
padding: 40px 60px;
border-radius: 10px;
box-shadow: var(--shadow-base);
}
.page-card .page-card-head {
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid #d1d8dd;
border-bottom: 1px solid var(--border-color);
}
.for-reset-password .page-card .page-card-head {
border-bottom: 0;
}
.page-card-head h4 {
font-size: 18px;
font-weight: 600;
}
#reset-password .form-group {
margin-bottom: 10px;
font-size: var(--font-size-sm);
}
.page-card .page-card-head .indicator {
color: #36414C;
font-size: 14px;
}
.sign-up-message {
margin-top: 20px;
font-size: 13px;
color: var(--text-color);
}
.page-card .page-card-head .indicator::before {
margin: 0 6px 0.5px 0px;
}
button#update {
font-size: var(--font-size-sm);
}
.page-card .btn {
margin-top: 30px;
}
.page-card p {
font-size: 14px;
}

View file

@ -4,6 +4,7 @@ import frappe, unittest
from frappe.model.db_query import DatabaseQuery
from frappe.desk.reportview import get_filters_cond
from frappe.query_builder import Column
from frappe.core.page.permission_manager.permission_manager import update, reset, add
from frappe.permissions import add_user_permission, clear_user_permissions_for_doctype
@ -373,6 +374,25 @@ class TestReportview(unittest.TestCase):
owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner")
self.assertEqual(owners, ["Administrator"])
def test_column_comparison(self):
"""Test DatabaseQuery.execute to test column comparison
"""
users_unedited = frappe.get_all(
"User",
filters={"creation": Column("modified")},
fields=["name", "creation", "modified"],
limit=1,
)
users_edited = frappe.get_all(
"User",
filters={"creation": ("!=", Column("modified"))},
fields=["name", "creation", "modified"],
limit=1,
)
self.assertEqual(users_unedited[0].modified, users_unedited[0].creation)
self.assertNotEqual(users_edited[0].modified, users_edited[0].creation)
def test_reportview_get(self):
user = frappe.get_doc("User", "test@example.com")
add_child_table_to_blog_post()

View file

@ -5,7 +5,7 @@ import redis
import frappe
from frappe.utils import get_bench_id
from frappe.utils.rq import RedisQueue
from frappe.utils.redis_queue import RedisQueue
from frappe.utils.background_jobs import get_redis_conn
def version_tuple(version):

View file

@ -16,7 +16,7 @@ import frappe
from frappe import _
import frappe.monitor
from frappe.utils import cstr, get_bench_id
from frappe.utils.rq import RedisQueue
from frappe.utils.redis_queue import RedisQueue
from frappe.utils.commands import log

View file

@ -98,8 +98,8 @@ def report_to_pdf(html, orientation="Landscape"):
frappe.local.response.type = "pdf"
@frappe.whitelist()
def print_by_server(doctype, name, print_format=None, doc=None, no_letterhead=0):
print_settings = frappe.get_doc("Print Settings")
def print_by_server(doctype, name, printer_setting, print_format=None, doc=None, no_letterhead=0):
print_settings = frappe.get_doc("Network Printer Settings", printer_setting)
try:
import cups
except ImportError:
@ -123,4 +123,4 @@ def print_by_server(doctype, name, print_format=None, doc=None, no_letterhead=0)
except cups.IPPError:
frappe.throw(_("Printing failed"))
finally:
cleanup(file,{})
return

View file

@ -16,15 +16,26 @@ class TestWebForm(unittest.TestCase):
def tearDown(self):
frappe.conf.disable_website_cache = False
frappe.local.path = None
frappe.local.request_ip = None
frappe.form_dict.web_form = None
frappe.form_dict.data = None
frappe.form_dict.docname = None
def test_accept(self):
frappe.set_user("Administrator")
accept(web_form='manage-events', data=json.dumps({
doc = {
'doctype': 'Event',
'subject': '_Test Event Web Form',
'description': '_Test Event Description',
'starts_on': '2014-09-09'
}))
}
frappe.form_dict.web_form = "manage-events"
frappe.form_dict.data = json.dumps(doc)
frappe.local.request_ip = '127.0.0.1'
accept(web_form='manage-events', data=json.dumps(doc))
self.event_name = frappe.db.get_value("Event",
{"subject": "_Test Event Web Form"})
@ -32,6 +43,7 @@ class TestWebForm(unittest.TestCase):
def test_edit(self):
self.test_accept()
doc={
'doctype': 'Event',
'subject': '_Test Event Web Form',
@ -43,6 +55,10 @@ class TestWebForm(unittest.TestCase):
self.assertNotEqual(frappe.db.get_value("Event",
self.event_name, "description"), doc.get('description'))
frappe.form_dict.web_form = 'manage-events'
frappe.form_dict.docname = self.event_name
frappe.form_dict.data = json.dumps(doc)
accept(web_form='manage-events', docname=self.event_name, data=json.dumps(doc))
self.assertEqual(frappe.db.get_value("Event",

View file

@ -13,7 +13,7 @@ from frappe.modules.utils import export_module_json, get_doc_module
from frappe.utils import cstr
from frappe.website.utils import get_comment_list
from frappe.website.website_generator import WebsiteGenerator
from frappe.rate_limiter import rate_limit
class WebForm(WebsiteGenerator):
website = frappe._dict(
@ -365,6 +365,7 @@ def get_context(context):
@frappe.whitelist(allow_guest=True)
@rate_limit(key='web_form', limit=5, seconds=60, methods=['POST'])
def accept(web_form, data, docname=None, for_payment=False):
'''Save the web form'''
data = frappe._dict(json.loads(data))

View file

@ -9,9 +9,10 @@ from frappe.desk.form.utils import get_pdf_link
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe import _
from frappe.model.workflow import apply_workflow, get_workflow_name, has_approval_access, \
get_workflow_state_field, send_email_alert, get_workflow_field_value, is_transition_condition_satisfied
get_workflow_state_field, send_email_alert, is_transition_condition_satisfied
from frappe.desk.notifications import clear_doctype_notifications
from frappe.utils.user import get_users_with_role
from frappe.utils.data import get_link_to_form
class WorkflowAction(Document):
pass
@ -286,13 +287,13 @@ def get_common_email_args(doc):
subject = frappe.render_template(email_template.subject, vars(doc))
response = frappe.render_template(email_template.response, vars(doc))
else:
subject = _('Workflow Action')
response = _('{0}: {1}').format(doctype, docname)
subject = _('Workflow Action') + f" on {doctype}: {docname}"
response = get_link_to_form(doctype, docname, f"{doctype}: {docname}")
common_args = {
'template': 'workflow_action',
'header': 'Workflow Action',
'attachments': [frappe.attach_print(doctype, docname , file_name=docname)],
'attachments': [frappe.attach_print(doctype, docname, file_name=docname, doc=doc)],
'subject': subject,
'message': response
}

View file

@ -1,32 +1,38 @@
{% extends "templates/web.html" %}
{% block title %} {{_("Reset Password")}} {% endblock %}
{% block head_include %}
{% endblock %}
{% block page_content %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}</span>
<section class="for-reset-password d-block">
<div class="page-card">
<div class='page-card-head text-center'>
<h4 class="reset-password-heading">{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}</h4>
</div>
<form id="reset-password">
<div class="form-group" style="display: none;">
<input id="old_password" type="password"
class="form-control" placeholder="{{ _("Old Password") }}">
</div>
<div class="form-group">
<input id="new_password" type="password"
class="form-control" placeholder="{{ _("New Password") }}">
<span class="password-strength-indicator indicator"></span>
</div>
<p class='password-strength-message text-muted small hidden'></p>
<button type="submit" id="update"
class="btn btn-primary btn-block btn-update">{{_("Confirm")}}</button>
</form>
{%- if not disable_signup -%}
<div class="text-center sign-up-message">
{{ _("Don't have an account?") }}
<a href="/login#signup">{{ _("Sign up") }}</a>
</div>
{%- endif -%}
</div>
<form id="reset-password">
<div class="form-group" style="display: none;">
<input id="old_password" type="password"
class="form-control" placeholder="{{ _("Old Password") }}">
</div>
<div class="form-group">
<input id="new_password" type="password"
class="form-control" placeholder="{{ _("New Password") }}">
<span class="password-strength-indicator indicator"></span>
</div>
<p class='password-strength-message text-muted small hidden'></p>
<button type="submit" id="update"
class="btn btn-primary">{{_("Update")}}</button>
</form>
</div>
</section>
<style>
.hero-and-content {
background-color: #f5f7fa;
}
</style>
<script>
@ -69,20 +75,19 @@ frappe.ready(function() {
args: args,
statusCode: {
401: function() {
$(".page-card-head .indicator").removeClass().addClass("indicator red").text(__("Invalid Password"));
$(".page-card-head .reset-password-heading").text(__("Invalid Password"));
},
410: function({ responseJSON }) {
const title = __("Invalid Link");
const message = responseJSON.message;
$(".page-card-head .indicator").removeClass().addClass("indicator grey").text(title);
$(".page-card-head .reset-password-heading").text(title);
frappe.msgprint({ title: title, message: message, clear: true });
},
200: function(r) {
$("input").val("");
strength_indicator.addClass("hidden");
strength_message.addClass("hidden");
$(".page-card-head .indicator")
.removeClass().addClass("indicator blue")
$(".page-card-head .reset-password-heading")
.html(__("Status Updated"));
if(r.message) {
frappe.msgprint({
@ -132,7 +137,7 @@ frappe.ready(function() {
},
statusCode: {
401: function() {
$('.page-card-head .indicator').removeClass().addClass('indicator red')
$('.page-card-head .reset-password-heading')
.text("{{ _('Invalid Password') }}");
},
200: function(r) {
@ -175,7 +180,6 @@ frappe.ready(function() {
}
}
strength_indicator.removeClass().addClass('password-strength-indicator indicator ' + color);
strength_message.html(message.join(' ') || '').removeClass('hidden');
// strength_indicator.attr('title', message.join(' ') || '');
}

View file

@ -24,8 +24,7 @@ googlemaps~=4.4.5
gunicorn~=20.1.0
html2text==2020.1.16
html5lib~=1.1
ipython~=7.16.1
jedi==0.17.2 # not directly required. Pinned to fix upstream IPython issue (https://github.com/ipython/ipython/issues/12740)
ipython~=7.27.0
Jinja2~=3.0.1
ldap3~=2.9
markdown2~=2.4.0

View file

@ -57,5 +57,5 @@ setup(
{
'clean': CleanCommand
},
python_requires='>=3.6'
python_requires='>=3.7'
)

View file

@ -2938,9 +2938,9 @@ normalize-url@^4.5.0:
set-blocking "~2.0.0"
nth-check@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
version "2.0.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==
dependencies:
boolbase "^1.0.0"