Merge branch 'develop' of https://github.com/frappe/frappe into fix-document-signature

This commit is contained in:
Suraj Shetty 2022-02-28 16:05:52 +05:30
commit 105cf91be8
96 changed files with 1268 additions and 791 deletions

View file

@ -13,3 +13,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
# Updating license headers
34460265554242a8d05fb09f049033b1117e1a2b
# Refactor "not a in b" -> "a not in b"
745297a49d516e5e3c4bb3e1b0c4235e7d31165d

View file

@ -50,7 +50,9 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench setup requirements --dev
if [ "$TYPE" == "ui" ]; then sed -i 's/^web: bench serve/web: bench serve --with-coverage/g' Procfile; fi
# install node-sass which is required for website theme test
cd ./apps/frappe || exit
@ -60,4 +62,4 @@ cd ../..
bench start &
bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
CI=Yes bench build --app frappe
if [ "$TYPE" == "server" ]; then CI=Yes bench build --app frappe; fi

View file

@ -41,6 +41,7 @@ if __name__ == "__main__":
# this is a push build, run all builds
if not pr_number:
os.system('echo "::set-output name=build::strawberry"')
os.system('echo "::set-output name=build-server::strawberry"')
sys.exit(0)
files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
@ -52,7 +53,8 @@ if __name__ == "__main__":
ci_files_changed = any(f for f in files_list if is_ci(f))
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
updated_py_file_count = len(list(filter(is_py, files_list)))
only_py_changed = updated_py_file_count == len(files_list)
if ci_files_changed:
print("CI related files were updated, running all build processes.")
@ -65,8 +67,12 @@ if __name__ == "__main__":
print("Only Frontend code was updated; Stopping Python build process.")
sys.exit(0)
elif only_py_changed and build_type == "ui":
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif build_type == "ui":
if only_py_changed:
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif updated_py_file_count > 0:
# both frontend and backend code were updated
os.system('echo "::set-output name=build-server::strawberry"')
os.system('echo "::set-output name=build::strawberry"')

View file

@ -141,6 +141,12 @@ jobs:
env:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
- name: Stop server
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
run: |
ps -ef | grep "frappe serve" | awk '{print $2}' | xargs kill -s SIGINT 2> /dev/null || true
sleep 5
- name: Check If Coverage Report Exists
id: check_coverage
uses: andstor/file-existence-action@v1
@ -156,3 +162,13 @@ jobs:
directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
verbose: true
flags: ui-tests
- name: Upload Server Coverage Data
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
flags: server

View file

@ -27,7 +27,7 @@
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a>
<a href="https://codecov.io/gh/frappe/frappe">
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj&flag=server"/>
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
</a>
</div>

View file

@ -0,0 +1,57 @@
context('Control Autocomplete', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
function get_dialog_with_autocomplete(options) {
cy.visit('/app/website');
return cy.dialog({
title: 'Autocomplete',
fields: [
{
'label': 'Select an option',
'fieldname': 'autocomplete',
'fieldtype': 'Autocomplete',
'options': options || ['Option 1', 'Option 2', 'Option 3'],
}
]
});
}
it('should set the valid value', () => {
get_dialog_with_autocomplete().as('dialog');
cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
cy.wait(1000);
cy.get('@input').type('2', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('autocomplete');
expect(value).to.eq('Option 2');
dialog.clear();
});
});
it('should set the valid value with different label', () => {
const options_with_label = [
{ label: "Option 1", value: "option_1" },
{ label: "Option 2", value: "option_2" }
];
get_dialog_with_autocomplete(options_with_label).as('dialog');
cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
cy.get('@input').type('2', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('autocomplete');
expect(value).to.eq('option_2');
dialog.clear();
});
});
});

View file

@ -55,10 +55,31 @@ context('Depends On', () => {
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
'options': "Child Test Depends On"
},
{
"label": "Dependent Tab",
"fieldname": "dependent_tab",
"fieldtype": "Tab Break",
"depends_on": "eval:doc.test_field=='Show Tab'"
},
{
"fieldname": "tab_section",
"fieldtype": "Section Break",
},
{
"label": "Field in Tab",
"fieldname": "field_in_tab",
"fieldtype": "Data",
}
]
});
});
});
it('should show the tab on other setting field value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Show Tab');
cy.get('body').click();
cy.findByRole("tab", {name: "Dependent Tab"}).should('be.visible');
});
it('should set the field as mandatory depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Some Value');

View file

@ -12,6 +12,7 @@ context('List View', () => {
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
cy.get('.actions-btn-group button').contains('Actions').should('be.visible');
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh');
cy.wait(3000); // wait before you hit another refresh
cy.get('button[data-original-title="Refresh"]').click();
cy.wait('@list-refresh');
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');

View file

@ -29,6 +29,7 @@ context('Report View', () => {
// select the cell
cell.dblclick();
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
cy.get('.dt-row-0 > .dt-cell--col-3').click(); // click outside
cy.wait('@value-update');
@ -70,4 +71,4 @@ context('Report View', () => {
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
});
});
});

View file

@ -1,25 +1,21 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import re
import json
import shutil
import re
import subprocess
from subprocess import getoutput
from io import StringIO
from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable
import frappe
from frappe.utils.minify import JavascriptMinify
from subprocess import getoutput
from tempfile import mkdtemp, mktemp
from urllib.parse import urlparse
import click
import psutil
from urllib.parse import urlparse
from semantic_version import Version
from requests import head
from requests.exceptions import HTTPError
from semantic_version import Version
import frappe
timestamps = {}
app_paths = None
@ -32,6 +28,7 @@ class AssetsNotDownloadedError(Exception):
class AssetsDontExistError(HTTPError):
pass
def download_file(url, prefix):
from requests import get
@ -277,12 +274,14 @@ def check_node_executable():
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo()
def get_node_env():
node_env = {
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
}
return node_env
def get_safe_max_old_space_size():
safe_max_old_space_size = 0
try:
@ -296,6 +295,7 @@ def get_safe_max_old_space_size():
return safe_max_old_space_size
def generate_assets_map():
symlinks = {}
@ -344,7 +344,6 @@ def clear_broken_symlinks():
os.remove(path)
def unstrip(message: str) -> str:
"""Pads input string on the right side until the last available column in the terminal
"""
@ -397,94 +396,6 @@ def link_assets_dir(source, target, hard_link=False):
symlink(source, target, overwrite=True)
def build(no_compress=False, verbose=False):
for target, sources in get_build_maps().items():
pack(os.path.join(assets_path, target), sources, no_compress, verbose)
def get_build_maps():
"""get all build.jsons with absolute paths"""
# framework js and css files
build_maps = {}
for app_path in app_paths:
path = os.path.join(app_path, "public", "build.json")
if os.path.exists(path):
with open(path) as f:
try:
for target, sources in (json.loads(f.read() or "{}")).items():
# update app path
source_paths = []
for source in sources:
if isinstance(source, list):
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
else:
s = os.path.join(app_path, source)
source_paths.append(s)
build_maps[target] = source_paths
except ValueError as e:
print(path)
print("JSON syntax error {0}".format(str(e)))
return build_maps
def pack(target, sources, no_compress, verbose):
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()
for f in sources:
suffix = None
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
print("did not find " + f)
continue
timestamps[f] = os.path.getmtime(f)
try:
with open(f, "r") as sourcefile:
data = str(sourcefile.read(), "utf-8", errors="ignore")
extn = f.rsplit(".", 1)[1]
if (
outtype == "js"
and extn == "js"
and (not no_compress)
and suffix != "concat"
and (".min." not in f)
):
tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO()
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
outtxt += str(minified or "", "utf-8").strip("\n") + ";"
if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
elif outtype == "js" and extn == "html":
# add to frappe.templates
outtxt += html_to_js_template(f, data)
else:
outtxt += "\n/*\n *\t%s\n */" % f
outtxt += "\n" + data + "\n"
except Exception:
print("--Error in:" + f + "--")
print(frappe.get_traceback())
with open(target, "w") as f:
f.write(outtxt.encode("utf-8"))
print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024))))
def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
def scrub_html_template(content):
"""Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space
@ -496,37 +407,7 @@ def scrub_html_template(content):
return content.replace("'", "\'")
def files_dirty():
for target, sources in get_build_maps().items():
for f in sources:
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
continue
if os.path.getmtime(f) != timestamps.get(f):
print(f + " dirty")
return True
else:
return False
def compile_less():
if not find_executable("lessc"):
return
for path in app_paths:
less_path = os.path.join(path, "public", "less")
if os.path.exists(less_path):
for fname in os.listdir(less_path):
if fname.endswith(".less") and fname != "variables.less":
fpath = os.path.join(less_path, fname)
mtime = os.path.getmtime(fpath)
if fpath in timestamps and mtime == timestamps[fpath]:
continue
timestamps[fpath] = mtime
print("compiling {0}".format(fpath))
css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
os.system("lessc {0} > {1}".format(fpath, css_path))
def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))

View file

@ -19,36 +19,38 @@ from frappe.exceptions import SiteNotSpecifiedError
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--db-host', help='Database Host')
@click.option('--db-port', type=int, help='Database Port')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--no-mariadb-socket', is_flag=True, default=False, help='Set MariaDB host to % and use TCP/IP Socket instead of using the UNIX Socket')
@click.option('--admin-password', help='Administrator password for new site', default=None)
@click.option('--verbose', is_flag=True, default=False, help='Verbose')
@click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False)
@click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None):
@click.option('--set-default', is_flag=True, default=False, help='Set the new site as default site')
def new_site(site, db_root_username=None, db_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None,
set_default=False):
"Create a new site"
from frappe.installer import _new_site
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
db_port=db_port, new_site=True)
_new_site(db_name, site, db_root_username=db_root_username,
db_root_password=db_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
db_port=db_port, new_site=True)
if len(frappe.utils.get_sites()) == 1:
if set_default:
use(site)
@click.command('restore')
@click.argument('sql-file-path')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--db-name', help='Database name for site in case it is a new one')
@click.option('--admin-password', help='Administrator password for new site')
@click.option('--install-app', multiple=True, help='Install app after installation')
@ -57,7 +59,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
@click.option('--encryption-key', help='Backup encryption key')
@pass_context
def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None,
def restore(context, sql_file_path, encryption_key=None, db_root_username=None, db_root_password=None,
db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None,
with_private_files=None):
"Restore site database from an sql file"
@ -150,8 +152,8 @@ def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=N
try:
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
_new_site(frappe.conf.db_name, site, db_root_username=db_root_username,
db_root_password=db_root_password, admin_password=admin_password,
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
force=True, db_type=frappe.conf.db_type)
@ -290,16 +292,16 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
@click.command('reinstall')
@click.option('--admin-password', help='Administrator Password for reinstalled site')
@click.option('--mariadb-root-username', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--yes', is_flag=True, default=False, help='Pass --yes to skip confirmation')
@pass_context
def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False):
def reinstall(context, admin_password=None, db_root_username=None, db_root_password=None, yes=False):
"Reinstall site ie. wipe all data and start over"
site = get_site(context)
_reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose)
_reinstall(site, admin_password, db_root_username, db_root_password, yes, verbose=context.verbose)
def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False):
def _reinstall(site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False):
from frappe.installer import _new_site
if not yes:
@ -319,7 +321,7 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
frappe.init(site=site)
_new_site(frappe.conf.db_name, site, verbose=verbose, force=True, reinstall=True, install_apps=installed,
mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password,
db_root_username=db_root_username, db_root_password=db_root_password,
admin_password=admin_password)
@click.command('install-app')
@ -447,21 +449,17 @@ def disable_user(context, email):
@pass_context
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
from frappe.migrate import migrate
from frappe.migrate import SiteMigration
for site in context.sites:
click.secho(f"Migrating {site}", fg="green")
frappe.init(site=site)
frappe.connect()
try:
migrate(
context.verbose,
SiteMigration(
skip_failing=skip_failing,
skip_search_index=skip_search_index
)
skip_search_index=skip_search_index,
).run(site=site)
finally:
print()
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@ -660,16 +658,16 @@ def uninstall(context, app, dry_run, yes, no_backup, force):
@click.command('drop-site')
@click.argument('site')
@click.option('--root-login', default='root')
@click.option('--root-password')
@click.option('--db-root-username', '--mariadb-root-username', '--root-login', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', '--root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--archived-sites-path')
@click.option('--no-backup', is_flag=True, default=False)
@click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False)
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
_drop_site(site, root_login, root_password, archived_sites_path, force, no_backup)
def drop_site(site, db_root_username='root', db_root_password=None, archived_sites_path=None, force=False, no_backup=False):
_drop_site(site, db_root_username, db_root_password, archived_sites_path, force, no_backup)
def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
def _drop_site(site, db_root_username=None, db_root_password=None, archived_sites_path=None, force=False, no_backup=False):
"Remove site from database and filesystem"
from frappe.database import drop_user_and_database
from frappe.utils.backups import scheduled_backup
@ -694,7 +692,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
click.echo("\n".join(messages))
sys.exit(1)
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)
archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')

View file

@ -640,6 +640,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
skip_test_records=False, skip_before_tests=False, failfast=False, case=None):
with CodeCoverage(coverage, app):
import frappe
import frappe.test_runner
tests = test
site = get_site(context)
@ -742,8 +743,9 @@ def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=Fals
@click.option('--profile', is_flag=True, default=False)
@click.option('--noreload', "no_reload", is_flag=True, default=False)
@click.option('--nothreading', "no_threading", is_flag=True, default=False)
@click.option('--with-coverage', is_flag=True, default=False)
@pass_context
def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None):
def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None, with_coverage=False):
"Start development web server"
import frappe.app
@ -751,8 +753,12 @@ def serve(context, port=None, profile=False, no_reload=False, no_threading=False
site = None
else:
site = context.sites[0]
frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
with CodeCoverage(with_coverage, 'frappe'):
if with_coverage:
# unable to track coverage with threading enabled
no_threading = True
no_reload = True
frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
@click.command('request')

View file

@ -99,7 +99,7 @@
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
},
@ -547,7 +547,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-27 21:22:20.529072",
"modified": "2022-02-14 11:56:19.812863",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -33,9 +33,16 @@ frappe.ui.form.on('DocType', {
}
}
const customize_form_link = "<a href='/app/customize-form'>Customize Form</a>";
if(!frappe.boot.developer_mode && !frm.doc.custom) {
// make the document read-only
frm.set_read_only();
frm.dashboard.add_comment(__("DocTypes can not be modified, please use {0} instead", [customize_form_link]), "blue", true);
} else if (frappe.boot.developer_mode) {
let msg = __("This site is running in developer mode. Any change made here will be updated in code.");
msg += "<br>";
msg += __("If you just want to customize for your site, use {0} instead.", [customize_form_link]);
frm.dashboard.add_comment(msg, "yellow");
}
if(frm.is_new()) {

View file

@ -786,9 +786,10 @@ def validate_links_table_fieldnames(meta):
fieldnames = tuple(field.fieldname for field in meta.fields)
for index, link in enumerate(meta.links, 1):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
if not frappe.get_meta(link.link_doctype).has_field(link.link_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(
index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)
)
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
if not link.is_child_table:
@ -802,8 +803,15 @@ def validate_links_table_fieldnames(meta):
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
if link.table_fieldname not in fieldnames:
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
if meta.name == link.parent_doctype:
field_exists = link.table_fieldname in fieldnames
else:
field_exists = frappe.get_meta(link.parent_doctype).has_field(link.table_fieldname)
if not field_exists:
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(
index, frappe.bold(link.table_fieldname), frappe.bold(meta.name)
)
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
def validate_fields_for_doctype(doctype):

View file

@ -498,6 +498,13 @@ class TestDocType(unittest.TestCase):
self.assertEqual(doc.is_virtual, 1)
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))
def test_default_fieldname(self):
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}]
dt = new_doctype("DT with default field", fields=fields)
dt.insert()
dt.delete()
def new_doctype(name, unique=0, depends_on='', fields=None):
doc = frappe.get_doc({
"doctype": "DocType",

View file

@ -3,7 +3,7 @@
import frappe, json, os
import unittest
from frappe.desk.query_report import run, save_report
from frappe.desk.query_report import run, save_report, add_total_row
from frappe.desk.reportview import delete_report, save_report as _save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
from frappe.core.doctype.user_permission.test_user_permission import create_user
@ -282,3 +282,56 @@ result = [
# Set user back to administrator
frappe.set_user('Administrator')
def test_add_total_row_for_tree_reports(self):
report_settings = {
'tree': True,
'parent_field': 'parent_value'
}
columns = [
{
"fieldname": "parent_column",
"label": "Parent Column",
"fieldtype": "Data",
"width": 10
},
{
"fieldname": "column_1",
"label": "Column 1",
"fieldtype": "Float",
"width": 10
},
{
"fieldname": "column_2",
"label": "Column 2",
"fieldtype": "Float",
"width": 10
}
]
result = [
{
"parent_column": "Parent 1",
"column_1": 200,
"column_2": 150.50
},
{
"parent_column": "Child 1",
"column_1": 100,
"column_2": 75.25,
"parent_value": "Parent 1"
},
{
"parent_column": "Child 2",
"column_1": 100,
"column_2": 75.25,
"parent_value": "Parent 1"
}
]
result = add_total_row(result, columns, meta=None, is_tree=report_settings['tree'],
parent_field=report_settings['parent_field'])
self.assertEqual(result[-1][0], "Total")
self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50)

View file

@ -356,7 +356,7 @@ class TestUser(unittest.TestCase):
self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/")
update_password(old_password, old_password=new_password)
self.assertEqual(
json.loads(frappe.message_log[0]).get("message"),
json.loads(frappe.message_log[0]).get("message"),
"Password reset instructions have been sent to your email"
)

View file

@ -29,6 +29,7 @@ FRAPPE_EXCLUSIONS = [
"*/commands/*",
"*/frappe/change_log/*",
"*/frappe/exceptions*",
"*/frappe/coverage.py",
"*frappe/setup.py",
"*/doctype/*/*_dashboard.py",
"*/patches/*",

View file

@ -2,6 +2,9 @@
// For license information, please see license.txt
frappe.ui.form.on('Client Script', {
setup(frm) {
frm.get_field("sample").html(SAMPLE_HTML);
},
refresh(frm) {
if (frm.doc.dt && frm.doc.script) {
frm.add_custom_button(__('Go to {0}', [frm.doc.dt]),
@ -97,3 +100,56 @@ frappe.ui.form.on('${doctype}', {
frm.set_value('script', script + boilerplate);
}
});
const SAMPLE_HTML = `<h3>Client Script Help</h3>
<p>Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>
<pre><code>
// fetch local_tax_no on selection of customer
// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname);
cur_frm.add_fetch("customer", "local_tax_no', 'local_tax_no');
// additional validation on dates
frappe.ui.form.on('Task', 'validate', function(frm) {
if (frm.doc.from_date &lt; get_today()) {
msgprint('You can not select past date in From Date');
validated = false;
}
});
// make a field read-only after saving
frappe.ui.form.on('Task', {
refresh: function(frm) {
// use the __islocal value of doc, to check if the doc is saved or not
frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);
}
});
// additional permission check
frappe.ui.form.on('Task', {
validate: function(frm) {
if(user=='user1@example.com' &amp;&amp; frm.doc.purpose!='Material Receipt') {
msgprint('You are only allowed Material Receipt');
validated = false;
}
}
});
// calculate sales incentive
frappe.ui.form.on('Sales Invoice', {
validate: function(frm) {
// calculate incentives for each person on the deal
total_incentive = 0
$.each(frm.doc.sales_team, function(i, d) {
// calculate incentive
var incentive_percent = 2;
if(frm.doc.base_grand_total &gt; 400) incentive_percent = 4;
// actual incentive
d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
total_incentive += flt(d.incentives)
});
frm.doc.total_incentive = total_incentive;
}
})
</code></pre>`;

View file

@ -40,8 +40,7 @@
{
"fieldname": "sample",
"fieldtype": "HTML",
"label": "Sample",
"options": "<h3>Client Script Help</h3>\n<p>Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>\n<pre><code>\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date &lt; get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' &amp;&amp; frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total &gt; 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n</code></pre>"
"label": "Sample"
},
{
"default": "0",
@ -76,7 +75,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-04 12:03:27.029815",
"modified": "2022-02-18 00:43:33.941466",
"modified_by": "Administrator",
"module": "Custom",
"name": "Client Script",
@ -107,5 +106,6 @@
],
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -122,7 +122,7 @@
"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",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
@ -431,7 +431,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-27 21:47:01.065556",
"modified": "2022-02-14 15:42:21.885999",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -540,6 +540,7 @@ docfield_properties = {
'in_global_search': 'Check',
'in_preview': 'Check',
'bold': 'Check',
'no_copy': 'Check',
'hidden': 'Check',
'collapsible': 'Check',
'collapsible_depends_on': 'Data',
@ -599,4 +600,4 @@ ALLOWED_FIELDTYPE_CHANGE = (
('Code', 'Geolocation'),
('Table', 'Table MultiSelect'))
ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data')
ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Data')

View file

@ -97,13 +97,18 @@ class TestCustomizeForm(unittest.TestCase):
custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0]
custom_field.reqd = 1
custom_field.no_copy = 1
d.run_method("save_customization")
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 1)
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 1)
custom_field = d.get("fields", {"is_custom_field": True})[0]
custom_field.reqd = 0
custom_field.no_copy = 0
d.run_method("save_customization")
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 0)
def test_save_customization_new_field(self):
d = self.get_customize_form("Event")
@ -257,7 +262,7 @@ class TestCustomizeForm(unittest.TestCase):
frappe.clear_cache()
d = self.get_customize_form("User Group")
d.append('links', dict(link_doctype='User Group Member', parent_doctype='User',
d.append('links', dict(link_doctype='User Group Member', parent_doctype='User Group',
link_fieldname='user', table_fieldname='user_group_members', group='Tests', custom=1))
d.run_method("save_customization")
@ -267,7 +272,7 @@ class TestCustomizeForm(unittest.TestCase):
# check links exist
self.assertTrue([d.name for d in user_group.links if d.link_doctype == 'User Group Member'])
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User'])
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User Group'])
# remove the link
d = self.get_customize_form("User Group")

View file

@ -20,6 +20,7 @@
"in_global_search",
"in_preview",
"bold",
"no_copy",
"allow_in_quick_entry",
"translatable",
"column_break_7",
@ -84,7 +85,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\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
},
@ -437,13 +438,19 @@
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Show Dashboard"
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-27 21:45:22.349776",
"modified": "2022-02-25 16:01:12.616736",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -584,7 +584,7 @@ class Database(object):
company = frappe.db.get_single_value('Global Defaults', 'default_company')
"""
if not doctype in self.value_cache:
if doctype not in self.value_cache:
self.value_cache[doctype] = {}
if cache and fieldname in self.value_cache[doctype]:

View file

@ -52,7 +52,8 @@ class MariaDBDatabase(Database):
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
'Icon': ('varchar', self.VARCHAR_LEN),
'Autocomplete': ('varchar', self.VARCHAR_LEN),
}
def get_connection(self):

View file

@ -62,7 +62,8 @@ class PostgresDatabase(Database):
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
'Icon': ('varchar', self.VARCHAR_LEN),
'Autocomplete': ('varchar', self.VARCHAR_LEN),
}
def get_connection(self):

View file

@ -5,29 +5,29 @@ from frappe.database.schema import DBTable, get_definition
class PostgresTable(DBTable):
def create(self):
add_text = ""
varchar_len = frappe.db.VARCHAR_LEN
additional_definitions = ""
# columns
column_defs = self.get_column_definitions()
if column_defs:
add_text += ",\n".join(column_defs)
additional_definitions += ",\n".join(column_defs)
# child table columns
if self.meta.get("istable") or 0:
if column_defs:
add_text += ",\n"
additional_definitions += ",\n"
add_text += ",\n".join(
additional_definitions += ",\n".join(
(
"parent varchar({varchar_len})",
"parentfield varchar({varchar_len})",
"parenttype varchar({varchar_len})"
f"parent varchar({varchar_len})",
f"parentfield varchar({varchar_len})",
f"parenttype varchar({varchar_len})",
)
)
# TODO: set docstatus length
# create table
frappe.db.sql(("""create table `%s` (
frappe.db.sql(f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
creation timestamp(6),
modified timestamp(6),
@ -35,7 +35,9 @@ class PostgresTable(DBTable):
owner varchar({varchar_len}),
docstatus smallint not null default '0',
idx bigint not null default '0',
%s)""" % (self.table_name, add_text)).format(varchar_len=frappe.db.VARCHAR_LEN))
{additional_definitions}
)"""
)
self.create_indexes()
frappe.db.commit()

View file

@ -4,7 +4,7 @@ import frappe
def setup_database(force, source_sql=None, verbose=False):
root_conn = get_root_connection()
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn.commit()
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name))
root_conn.sql("DROP USER IF EXISTS {0}".format(frappe.conf.db_name))
@ -70,7 +70,7 @@ def import_db_from_sql(source_sql=None, verbose=False):
print(f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}")
def setup_help_database(help_db_name):
root_conn = get_root_connection()
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(help_db_name))
root_conn.sql("DROP USER IF EXISTS {0}".format(help_db_name))
root_conn.sql("CREATE DATABASE `{0}`".format(help_db_name))

View file

@ -11,8 +11,10 @@ from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from frappe import _dict
from urllib.parse import quote
@frappe.whitelist()
def getdoc(doctype, name, user=None):
"""
@ -50,8 +52,11 @@ def getdoc(doctype, name, user=None):
doc.add_seen()
set_link_titles(doc)
if frappe.response.docs is None:
frappe.response = _dict({"docs": []})
frappe.response.docs.append(doc)
@frappe.whitelist()
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype"""

View file

@ -12,6 +12,15 @@ from frappe.translate import extract_messages_from_code, make_dict_from_messages
from frappe.utils import get_html_format
ASSET_KEYS = (
"__js", "__css", "__list_js", "__calendar_js", "__map_js",
"__linked_with", "__messages", "__print_formats", "__workflow_docs",
"__form_grid_templates", "__listview_template", "__tree_js",
"__dashboard", "__kanban_column_fields", '__templates',
'__custom_js', '__custom_list_js'
)
def get_meta(doctype, cached=True):
# don't cache for developer mode as js files, templates may be edited
if cached and not frappe.conf.developer_mode:
@ -34,6 +43,12 @@ class FormMeta(Meta):
super(FormMeta, self).__init__(doctype)
self.load_assets()
def set(self, key, value, *args, **kwargs):
if key in ASSET_KEYS:
self.__dict__[key] = value
else:
super(FormMeta, self).set(key, value, *args, **kwargs)
def load_assets(self):
if self.get('__assets_loaded', False):
return
@ -55,11 +70,7 @@ class FormMeta(Meta):
def as_dict(self, no_nulls=False):
d = super(FormMeta, self).as_dict(no_nulls=no_nulls)
for k in ("__js", "__css", "__list_js", "__calendar_js", "__map_js",
"__linked_with", "__messages", "__print_formats", "__workflow_docs",
"__form_grid_templates", "__listview_template", "__tree_js",
"__dashboard", "__kanban_column_fields", '__templates',
'__custom_js', '__custom_list_js'):
for k in ASSET_KEYS:
d[k] = self.get(k)
# d['fields'] = d.get('fields', [])
@ -172,7 +183,7 @@ class FormMeta(Meta):
WHERE doc_type=%s AND docstatus<2 and disabled=0""", (self.name,), as_dict=1,
update={"doctype":"Print Format"})
self.set("__print_formats", print_formats, as_value=True)
self.set("__print_formats", print_formats)
def load_workflows(self):
# get active workflow
@ -186,7 +197,7 @@ class FormMeta(Meta):
for d in workflow.get("states"):
workflow_docs.append(frappe.get_doc("Workflow State", d.state))
self.set("__workflow_docs", workflow_docs, as_value=True)
self.set("__workflow_docs", workflow_docs)
def load_templates(self):
@ -208,7 +219,7 @@ class FormMeta(Meta):
for content in self.get("__form_grid_templates").values():
messages = extract_messages_from_code(content)
messages = make_dict_from_messages(messages)
self.get("__messages").update(messages, as_value=True)
self.get("__messages").update(messages)
def load_dashboard(self):
self.set('__dashboard', self.get_dashboard_data())
@ -224,7 +235,7 @@ class FormMeta(Meta):
fields = [x['field_name'] for x in values]
fields = list(set(fields))
self.set("__kanban_column_fields", fields, as_value=True)
self.set("__kanban_column_fields", fields)
except frappe.PermissionError:
# no access to kanban board
pass

View file

@ -73,7 +73,7 @@ def get_report_result(report, filters):
return res
@frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None):
def generate_report_result(report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None):
user = user or frappe.session.user
filters = filters or []
@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
result = get_filtered_data(report.ref_doctype, columns, result, user)
if cint(report.add_total_row) and result and not skip_total_row:
result = add_total_row(result, columns)
result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field)
return {
"result": result,
@ -210,7 +210,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, is_tree=False, parent_field=None):
report = get_report_doc(report_name)
if not user:
user = frappe.session.user
@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
result = generate_report_result(report, filters, user, custom_columns)
result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field)
result["add_total_row"] = report.add_total_row and not result.get(
"skip_total_row", False
@ -435,9 +435,10 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
return result, column_widths
def add_total_row(result, columns, meta=None):
def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
total_row = [""] * len(columns)
has_percent = []
for i, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None
if isinstance(col, str):
@ -464,12 +465,12 @@ def add_total_row(result, columns, meta=None):
for row in result:
if i >= len(row):
continue
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(
cell
):
total_row[i] = flt(total_row[i]) + flt(cell)
if not (is_tree and row.get(parent_field)):
total_row[i] = flt(total_row[i]) + flt(cell)
if fieldtype == "Percent" and i not in has_percent:
has_percent.append(i)

View file

@ -533,7 +533,8 @@ def get_stats(stats, doctype, filters=None):
columns = []
for tag in tags:
if not tag in columns: continue
if tag not in columns:
continue
try:
tag_count = frappe.get_list(doctype,
fields=[tag, "count(*)"],
@ -612,7 +613,7 @@ def scrub_user_tags(tagcount):
alltags = t.split(',')
for tag in alltags:
if tag:
if not tag in rdict:
if tag not in rdict:
rdict[tag] = 0
rdict[tag] += tagdict[t]

View file

@ -15,7 +15,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
tree_method = frappe.get_attr(tree_method)
if not tree_method in frappe.whitelisted:
if tree_method not in frappe.whitelisted:
frappe.throw(_("Not Permitted"), frappe.PermissionError)
data = tree_method(doctype, parent, **filters)

View file

@ -20,4 +20,4 @@ def validate_route_conflict(doctype, name):
raise frappe.NameError
def slug(name):
return name.lower().replace(' ', '-')
return name.lower().replace(' ', '-')

View file

@ -20,11 +20,13 @@ class TestDomain(unittest.TestCase):
mail_domain = frappe.get_doc("Email Domain", "test.com")
mail_account = frappe.get_doc("Email Account", "Test")
# Initially, incoming_port is different in domain and account
self.assertNotEqual(mail_account.incoming_port, mail_domain.incoming_port)
# Ensure a different port
mail_account.incoming_port = int(mail_domain.incoming_port) + 5
mail_account.save()
# Trigger update of accounts using this domain
mail_domain.on_update()
mail_account = frappe.get_doc("Email Account", "Test")
mail_account.reload()
# After update, incoming_port in account should match the domain
self.assertEqual(mail_account.incoming_port, mail_domain.incoming_port)

View file

@ -14,8 +14,8 @@ from frappe.defaults import _clear_cache
def _new_site(
db_name,
site,
mariadb_root_username=None,
mariadb_root_password=None,
db_root_username=None,
db_root_password=None,
admin_password=None,
verbose=False,
install_apps=None,
@ -60,8 +60,8 @@ def _new_site(
installing = touch_file(get_site_path("locks", "installing.lock"))
install_db(
root_login=mariadb_root_username,
root_password=mariadb_root_password,
root_login=db_root_username,
root_password=db_root_password,
db_name=db_name,
admin_password=admin_password,
verbose=verbose,
@ -92,7 +92,7 @@ def _new_site(
print("*** Scheduler is", scheduler_status, "***")
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
def install_db(root_login=None, root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False):
import frappe.database
@ -101,6 +101,11 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
if not root_login and db_type == 'mariadb':
root_login='root'
elif not root_login and db_type == 'postgres':
root_login='postgres'
make_conf(db_name, site_config=site_config, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
frappe.flags.in_install_db = True
@ -184,7 +189,7 @@ def install_app(name, verbose=False, set_as_patched=True):
def add_to_installed_apps(app_name, rebuild_website=True):
installed_apps = frappe.get_installed_apps()
if not app_name in installed_apps:
if app_name not in installed_apps:
installed_apps.append(app_name)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.db.commit()
@ -529,10 +534,9 @@ def extract_sql_gzip(sql_gz_path):
import subprocess
try:
# dvf - decompress, verbose, force
original_file = sql_gz_path
decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
cmd = 'gzip --decompress --force < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True)
except Exception:
raise

View file

@ -38,6 +38,7 @@
"local_ca_certs_file",
"ldap_custom_settings_section",
"ldap_group_objectclass",
"ldap_custom_group_search",
"column_break_33",
"ldap_group_member_attribute",
"ldap_group_mappings_section",
@ -247,6 +248,12 @@
"fieldtype": "Data",
"label": "Group Object Class"
},
{
"description": "string value, i.e. {0} or uid={0},ou=users,dc=example,dc=com",
"fieldname": "ldap_custom_group_search",
"fieldtype": "Data",
"label": "Custom Group Search"
},
{
"description": "Requires any valid fdn path. i.e. ou=users,dc=example,dc=com",
"fieldname": "ldap_search_path_user",

View file

@ -45,10 +45,14 @@ class LDAPSettings(Document):
title=_("Misconfigured"))
if self.ldap_directory_server.lower() == 'custom':
if not self.ldap_group_member_attribute or not self.ldap_group_mappings_section:
frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'LDAP Group Mappings' are entered"),
if not self.ldap_group_member_attribute or not self.ldap_group_objectclass:
frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'Group Object Class' are entered"),
title=_("Misconfigured"))
if self.ldap_custom_group_search and "{0}" not in self.ldap_custom_group_search:
frappe.throw(_("Custom Group Search if filled needs to contain the user placeholder {0}, eg uid={0},ou=users,dc=example,dc=com"),
title=_("Misconfigured"))
else:
frappe.throw(_("LDAP Search String must be enclosed in '()' and needs to contian the user placeholder {0}, eg sAMAccountName={0}"))
@ -209,7 +213,8 @@ class LDAPSettings(Document):
ldap_object_class = self.ldap_group_objectclass
ldap_group_members_attribute = self.ldap_group_member_attribute
user_search_str = getattr(user, self.ldap_username_field).value
ldap_custom_group_search = self.ldap_custom_group_search or "{0}"
user_search_str = ldap_custom_group_search.format(getattr(user, self.ldap_username_field).value)
else:
# NOTE: depreciate this else path

View file

@ -1,30 +1,54 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
import os
import sys
from textwrap import dedent
import frappe
import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
import frappe.modules.patch_handler
import frappe.translate
from frappe.cache_manager import clear_global_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.database.schema import add_column
from frappe.desk.notifications import clear_notifications
from frappe.modules.patch_handler import PatchType
from frappe.modules.utils import sync_customizations
from frappe.search.website_search import build_index_for_all_routes
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
from frappe.utils.fixtures import sync_fixtures
from frappe.website.utils import clear_website_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes
from frappe.database.schema import add_column
from frappe.modules.patch_handler import PatchType
BENCH_START_MESSAGE = dedent(
"""
Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:
$ bench start
Otherwise, check the server logs and ensure that all the required services are running.
"""
)
def atomic(method):
def wrapper(*args, **kwargs):
try:
ret = method(*args, **kwargs)
frappe.db.commit()
return ret
except Exception:
frappe.db.rollback()
raise
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the current version, will:
return wrapper
class SiteMigration:
"""Migrate all apps to the current version, will:
- run before migrate hooks
- run patches
- sync doctypes (schema)
@ -35,70 +59,117 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False):
- sync languages
- sync web pages (from /www)
- run after migrate hooks
'''
"""
service_status = check_connection(redis_services=["redis_cache"])
if False in service_status.values():
for service in service_status:
if not service_status.get(service, True):
print("{} service is not running.".format(service))
print("""Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:
def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None:
self.skip_failing = skip_failing
self.skip_search_index = skip_search_index
$ bench start
Otherwise, check the server logs and ensure that all the required services are running.""")
sys.exit(1)
touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
try:
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
def setUp(self):
"""Complete setup required for site migration
"""
frappe.flags.touched_tables = set()
frappe.flags.in_migrate = True
self.touched_tables_file = frappe.get_site_path("touched_tables.json")
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
clear_global_cache()
if os.path.exists(self.touched_tables_file):
os.remove(self.touched_tables_file)
frappe.flags.in_migrate = True
def tearDown(self):
"""Run operations that should be run post schema updation processes
This should be executed irrespective of outcome
"""
frappe.translate.clear_cache()
clear_website_cache()
clear_notifications()
with open(self.touched_tables_file, "w") as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
if not self.skip_search_index:
print(f"Building search index for {frappe.local.site}")
build_index_for_all_routes()
frappe.publish_realtime("version-update")
frappe.flags.touched_tables.clear()
frappe.flags.in_migrate = False
@atomic
def pre_schema_updates(self):
"""Executes `before_migrate` hooks
"""
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('before_migrate', app_name=app):
for fn in frappe.get_hooks("before_migrate", app_name=app):
frappe.get_attr(fn)()
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync)
@atomic
def run_schema_updates(self):
"""Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files
"""
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync)
frappe.model.sync.sync_all()
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync)
frappe.translate.clear_cache()
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync)
@atomic
def post_schema_updates(self):
"""Execute pending migration tasks post patches execution & schema sync
This includes:
* Sync `Scheduled Job Type` and scheduler events defined in hooks
* Sync fixtures & custom scripts
* Sync in-Desk Module Dashboards
* Sync customizations: Custom Fields, Property Setters, Custom Permissions
* Sync Frappe's internal language master
* Sync Portal Menu Items
* Sync Installed Applications Version History
* Execute `after_migrate` hooks
"""
sync_jobs()
sync_fixtures()
sync_dashboards()
sync_customizations()
sync_languages()
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
# syncs static files
clear_website_cache()
# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
frappe.get_single("Portal Settings").sync_menu()
frappe.get_single("Installed Applications").update_versions()
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):
for fn in frappe.get_hooks("after_migrate", app_name=app):
frappe.get_attr(fn)()
if not skip_search_index:
# Run this last as it updates the current session
print('Building search index for {}'.format(frappe.local.site))
build_index_for_all_routes()
def required_services_running(self) -> bool:
"""Returns True if all required services are running. Returns False and prints
instructions to stdout when required services are not available.
"""
service_status = check_connection(redis_services=["redis_cache"])
are_services_running = all(service_status.values())
frappe.db.commit()
if not are_services_running:
for service in service_status:
if not service_status.get(service, True):
print(f"Service {service} is not running.")
print(BENCH_START_MESSAGE)
clear_notifications()
return are_services_running
frappe.publish_realtime("version-update")
frappe.flags.in_migrate = False
finally:
with open(touched_tables_file, 'w') as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
frappe.flags.touched_tables.clear()
def run(self, site: str):
"""Run Migrate operation on site specified. This method initializes
and destroys connections to the site database.
"""
if not self.required_services_running():
raise SystemExit(1)
if site:
frappe.init(site=site)
frappe.connect()
self.setUp()
try:
self.pre_schema_updates()
self.run_schema_updates()
finally:
self.post_schema_updates()
self.tearDown()
frappe.destroy()

View file

@ -35,7 +35,8 @@ data_fieldtypes = (
'Barcode',
'Geolocation',
'Duration',
'Icon'
'Icon',
'Autocomplete',
)
attachment_fieldtypes = (

View file

@ -115,14 +115,18 @@ class BaseDocument(object):
return self
def update_if_missing(self, d):
"""Set default values for fields without existing values"""
if isinstance(d, BaseDocument):
d = d.get_valid_dict()
if "doctype" in d:
self.set("doctype", d.get("doctype"))
for key, value in d.items():
# dont_update_if_missing is a list of fieldnames, for which, you don't want to set default value
if (self.get(key) is None) and (value is not None) and (key not in self.dont_update_if_missing):
if (
value is not None
and self.get(key) is None
# dont_update_if_missing is a list of fieldnames
# for which you don't want to set default value
and key not in self.dont_update_if_missing
):
self.set(key, value)
def get_db_value(self, key):

View file

@ -330,7 +330,7 @@ class DatabaseQuery(object):
table_name = table_name[7:]
if not table_name[0]=='`':
table_name = f"`{table_name}`"
if not table_name in self.tables:
if table_name not in self.tables:
self.append_table(table_name)
def append_table(self, table_name):
@ -428,7 +428,7 @@ class DatabaseQuery(object):
f = get_filter(self.doctype, f, additional_filters_config)
tname = ('`tab' + f.doctype + '`')
if not tname in self.tables:
if tname not in self.tables:
self.append_table(tname)
if 'ifnull(' in f.fieldname:

View file

@ -115,7 +115,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
# All the linked docs should be checked beforehand
frappe.enqueue('frappe.model.delete_doc.delete_dynamic_links',
doctype=doc.doctype, name=doc.name,
is_async=False if frappe.flags.in_test else True)
now=frappe.flags.in_test)
# clear cache for Document
doc.clear_cache()

View file

@ -1154,7 +1154,7 @@ class Document(BaseDocument):
for f in hooks:
add_to_return_value(self, f(self, method, *args, **kwargs))
return self._return_value
return self.__dict__.pop("_return_value", None)
return runner

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from typing import Optional
import frappe
from frappe import _
from frappe.utils import now_datetime, cint, cstr
@ -283,7 +284,7 @@ def get_default_naming_series(doctype):
return None
def validate_name(doctype, name, case=None, merge=False):
def validate_name(doctype: str, name: str, case: Optional[str] = None):
if not name:
frappe.throw(_("No Name Specified for {0}").format(doctype))
if name.startswith("New "+doctype):

View file

@ -1,48 +1,80 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from typing import TYPE_CHECKING, Dict, List, Optional
import frappe
from frappe import _, bold
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import validate_name
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
from frappe.query_builder import Field
from frappe.utils import cint
from frappe.utils.password import rename_password
from frappe.query_builder import Field
if TYPE_CHECKING:
from frappe.model.meta import Meta
@frappe.whitelist()
def update_document_title(doctype, docname, title_field=None, old_title=None, new_title=None, new_name=None, merge=False):
def update_document_title(
*,
doctype: str,
docname: str,
title: Optional[str] = None,
name: Optional[str] = None,
merge: bool = False,
**kwargs
) -> str:
"""
Update title from header in form view
"""
if docname and new_name and not docname == new_name:
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)
if old_title and new_title and not old_title == new_title:
# to maintain backwards API compatibility
updated_title = kwargs.get("new_title") or title
updated_name = kwargs.get("new_name") or name
# TODO: omit this after runtime type checking (ref: https://github.com/frappe/frappe/pull/14927)
for obj in [docname, updated_title, updated_name]:
if not isinstance(obj, (str, type(None))):
frappe.throw(f"{obj=} must be of type str or None")
doc = frappe.get_doc(doctype, docname)
doc.check_permission(permtype="write")
title_field = doc.meta.get_title_field()
title_updated = (title_field != "name") and (updated_title != doc.get(title_field))
name_updated = updated_name != doc.name
if name_updated:
docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge)
if title_updated:
try:
frappe.db.set_value(doctype, docname, title_field, new_title)
frappe.msgprint(_('Saved'), alert=True, indicator='green')
frappe.db.set_value(doctype, docname, title_field, updated_title)
frappe.msgprint(_("Saved"), alert=True, indicator="green")
except Exception as e:
if frappe.db.is_duplicate_entry(e):
frappe.throw(
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
title=_("Duplicate Name"),
exc=frappe.DuplicateEntryError
exc=frappe.DuplicateEntryError,
)
raise
return docname
def rename_doc(
doctype,
old,
new,
force=False,
merge=False,
ignore_permissions=False,
ignore_if_exists=False,
show_alert=True,
rebuild_search=True
):
doctype: str,
old: str,
new: str,
force: bool = False,
merge: bool = False,
ignore_permissions: bool = False,
ignore_if_exists: bool = False,
show_alert: bool = True,
rebuild_search: bool = True,
) -> str:
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link"."""
if not frappe.db.exists(doctype, old):
return
@ -79,7 +111,7 @@ def rename_doc(
update_user_settings(old, new, link_fields)
if doctype=='DocType':
rename_doctype(doctype, old, new, force)
rename_doctype(doctype, old, new)
update_customizations(old, new)
update_attachments(doctype, old, new)
@ -121,7 +153,7 @@ def rename_doc(
return new
def update_assignments(old, new, doctype):
def update_assignments(old: str, new: str, doctype: str) -> None:
old_assignments = frappe.parse_json(frappe.db.get_value(doctype, old, '_assign')) or []
new_assignments = frappe.parse_json(frappe.db.get_value(doctype, new, '_assign')) or []
common_assignments = list(set(old_assignments).intersection(new_assignments))
@ -143,7 +175,7 @@ def update_assignments(old, new, doctype):
unique_assignments = list(set(old_assignments + new_assignments))
frappe.db.set_value(doctype, new, '_assign', frappe.as_json(unique_assignments, indent=0))
def update_user_settings(old, new, link_fields):
def update_user_settings(old: str, new: str, link_fields: List[Dict]) -> None:
'''
Update the user settings of all the linked doctypes while renaming.
'''
@ -178,7 +210,7 @@ def update_user_settings(old, new, link_fields):
def update_customizations(old: str, new: str) -> None:
frappe.db.set_value("Custom DocPerm", {"parent": old}, "parent", new, update_modified=False)
def update_attachments(doctype, old, new):
def update_attachments(doctype: str, old: str, new: str) -> None:
try:
if old != "File Data" and doctype != "DocType":
frappe.db.sql("""update `tabFile` set attached_to_name=%s
@ -187,11 +219,11 @@ def update_attachments(doctype, old, new):
if not frappe.db.is_column_missing(e):
raise
def rename_versions(doctype, old, new):
def rename_versions(doctype: str, old: str, new: str) -> None:
frappe.db.sql("""UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""",
(new, doctype, old))
def rename_eps_records(doctype, old, new):
def rename_eps_records(doctype: str, old: str, new: str) -> None:
epl = frappe.qb.DocType("Energy Point Log")
(frappe.qb.update(epl)
.set(epl.reference_name, new)
@ -201,20 +233,20 @@ def rename_eps_records(doctype, old, new):
)
).run()
def rename_parent_and_child(doctype, old, new, meta):
def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None:
# rename the doc
frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, '%s'), (new, old))
update_autoname_field(doctype, new, meta)
update_child_docs(old, new, meta)
def update_autoname_field(doctype, new, meta):
def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None:
# update the value of the autoname field on rename of the docname
if meta.get('autoname'):
field = meta.get('autoname').split(':')
if field and field[0] == "field":
frappe.db.sql("UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], '%s'), (new, new))
def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
def validate_rename(doctype: str, new: str, meta: "Meta", merge: bool, force: bool, ignore_permissions: bool) -> str:
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
exists = (
frappe.qb.from_(doctype)
@ -226,27 +258,27 @@ def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
exists = exists[0] if exists else None
if merge and not exists:
frappe.msgprint(_("{0} {1} does not exist, select a new target to merge").format(doctype, new), raise_exception=1)
frappe.throw(_("{0} {1} does not exist, select a new target to merge").format(doctype, new))
if exists and exists != new:
# for fixing case, accents
exists = None
if (not merge) and exists:
frappe.msgprint(_("Another {0} with name {1} exists, select another name").format(doctype, new), raise_exception=1)
frappe.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new))
if not (ignore_permissions or frappe.permissions.has_permission(doctype, "write", raise_exception=False)):
frappe.msgprint(_("You need write permission to rename"), raise_exception=1)
frappe.throw(_("You need write permission to rename"))
if not (force or ignore_permissions) and not meta.allow_rename:
frappe.msgprint(_("{0} not allowed to be renamed").format(_(doctype)), raise_exception=1)
frappe.throw(_("{0} not allowed to be renamed").format(_(doctype)))
# validate naming like it's done in doc.py
new = validate_name(doctype, new, merge=merge)
new = validate_name(doctype, new)
return new
def rename_doctype(doctype, old, new, force=False):
def rename_doctype(doctype: str, old: str, new: str) -> None:
# change options for fieldtype Table, Table MultiSelect and Link
fields_with_options = ("Link",) + frappe.model.table_fields
@ -261,13 +293,13 @@ def rename_doctype(doctype, old, new, force=False):
# change parenttype for fieldtype Table
update_parenttype_values(old, new)
def update_child_docs(old, new, meta):
def update_child_docs(old: str, new: str, meta: "Meta") -> None:
# update "parent"
for df in meta.get_table_fields():
frappe.db.sql("update `tab%s` set parent=%s where parent=%s" \
% (df.options, '%s', '%s'), (new, old))
def update_link_field_values(link_fields, old, new, doctype):
def update_link_field_values(link_fields: List[Dict], old: str, new: str, doctype: str) -> None:
for field in link_fields:
if field['issingle']:
try:
@ -302,12 +334,12 @@ def update_link_field_values(link_fields, old, new, doctype):
if doctype=='DocType' and field['parent'] == old:
field['parent'] = new
def get_link_fields(doctype):
def get_link_fields(doctype: str) -> List[Dict]:
# get link fields from tabDocField
if not frappe.flags.link_fields:
frappe.flags.link_fields = {}
if not doctype in frappe.flags.link_fields:
if doctype not in frappe.flags.link_fields:
link_fields = frappe.db.sql("""\
select parent, fieldname,
(select issingle from tabDocType dt
@ -345,7 +377,7 @@ def get_link_fields(doctype):
return frappe.flags.link_fields[doctype]
def update_options_for_fieldtype(fieldtype, old, new):
def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
if frappe.conf.developer_mode:
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
doctype = frappe.get_doc("DocType", name)
@ -366,7 +398,7 @@ def update_options_for_fieldtype(fieldtype, old, new):
frappe.db.sql("""update `tabProperty Setter` set value=%s
where property='options' and value=%s""", (new, old))
def get_select_fields(old, new):
def get_select_fields(old: str, new: str) -> List[Dict]:
"""
get select type fields where doctype's name is hardcoded as
new line separated list
@ -410,7 +442,7 @@ def get_select_fields(old, new):
return select_fields
def update_select_field_values(old, new):
def update_select_field_values(old: str, new: str):
frappe.db.sql("""
update `tabDocField` set options=replace(options, %s, %s)
where
@ -433,7 +465,7 @@ def update_select_field_values(old, new):
(value like {0} or value like {1})"""
.format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new))
def update_parenttype_values(old, new):
def update_parenttype_values(old: str, new: str):
child_doctypes = frappe.db.get_all('DocField',
fields=['options', 'fieldname'],
filters={
@ -469,7 +501,7 @@ def update_parenttype_values(old, new):
for doctype in child_doctypes:
frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old))
def rename_dynamic_links(doctype, old, new):
def rename_dynamic_links(doctype: str, old: str, new: str):
for df in get_dynamic_link_map().get(doctype, []):
# dynamic link in single, just one value to check
if frappe.get_meta(df.parent).issingle:
@ -485,7 +517,7 @@ def rename_dynamic_links(doctype, old, new):
where {options}=%s and {fieldname}=%s""".format(parent = parent,
fieldname=df.fieldname, options=df.options), (new, doctype, old))
def bulk_rename(doctype, rows=None, via_console = False):
def bulk_rename(doctype: str, rows: Optional[List[List]] = None, via_console: bool = False) -> Optional[List[str]]:
"""Bulk rename documents
:param doctype: DocType to be renamed
@ -523,7 +555,7 @@ def bulk_rename(doctype, rows=None, via_console = False):
if not via_console:
return rename_log
def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None):
def update_linked_doctypes(doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: Optional[List] = None) -> None:
from frappe.model.utils.rename_doc import update_linked_doctypes
show_deprecation_warning("update_linked_doctypes")
@ -536,7 +568,7 @@ def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=N
)
def get_fetch_fields(doctype, linked_to, ignore_doctypes=None):
def get_fetch_fields(doctype: str, linked_to: str, ignore_doctypes: Optional[List] = None) -> List[Dict]:
from frappe.model.utils.rename_doc import get_fetch_fields
show_deprecation_warning("get_fetch_fields")
@ -544,7 +576,7 @@ def get_fetch_fields(doctype, linked_to, ignore_doctypes=None):
doctype=doctype, linked_to=linked_to, ignore_doctypes=ignore_doctypes
)
def show_deprecation_warning(funct):
def show_deprecation_warning(funct: str) -> None:
from click import secho
message = (
f"Function frappe.model.rename_doc.{funct} has been deprecated and "

View file

@ -117,7 +117,7 @@ def get_doc_files(files, start_path):
if os.path.isdir(os.path.join(doctype_path, docname)):
doc_path = os.path.join(doctype_path, docname, docname) + ".json"
if os.path.exists(doc_path):
if not doc_path in files:
if doc_path not in files:
files.append(doc_path)
return files

View file

@ -1,10 +1,14 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from itertools import product
from typing import Dict, List, Optional
import frappe
from frappe.model.rename_doc import get_link_fields
def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None):
def update_linked_doctypes(doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: Optional[List] = None):
"""
linked_doctype_info_list = list formed by get_fetch_fields() function
docname = Master DocType's name in which modification are made
@ -24,7 +28,7 @@ def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=N
)
def get_fetch_fields(doctype, linked_to, ignore_doctypes=None):
def get_fetch_fields(doctype: str, linked_to: str, ignore_doctypes: Optional[List] = None) -> List[Dict]:
"""
doctype = Master DocType in which the changes are being made
linked_to = DocType name of the field thats being updated in Master

View file

@ -115,10 +115,11 @@ def import_file_by_path(path: str,force: bool = False,data_import: bool = False,
if not force or db_modified_timestamp:
try:
stored_hash = frappe.db.get_value(doc["doctype"], doc["name"], "migration_hash")
stored_hash = None
if doc["doctype"] == "DocType":
stored_hash = frappe.db.get_value(doc["doctype"], doc["name"], "migration_hash")
except Exception:
frappe.flags.dt += [doc["doctype"]]
stored_hash = None
# if hash exists and is equal no need to update
if stored_hash and stored_hash == calculated_hash:

View file

@ -11,7 +11,26 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
set_options() {
if (this.df.options) {
let options = this.df.options || [];
this._data = this.parse_options(options);
this.set_data(options);
}
}
format_for_input(value) {
if (value == null) {
return "";
} else if (this._data && this._data.length) {
const item = this._data.find(i => i.value == value);
return item ? item.label : value;
} else {
return value;
}
}
get_input_value() {
if (this.$input) {
const label = this.$input.val();
const item = this._data?.find(i => i.label == label);
return item ? item.value : label;
}
}
@ -23,7 +42,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
autoFirst: true,
list: this.get_data(),
data: function(item) {
if (!(item instanceof Object)) {
if (typeof item !== 'object') {
var d = { value: item };
item = d;
}
@ -65,6 +84,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
};
}
init_option_cache() {
if (!this.$input.cache) {
this.$input.cache = {};
}
if (!this.$input.cache[this.doctype]) {
this.$input.cache[this.doctype] = {};
}
if (!this.$input.cache[this.doctype][this.df.fieldname]) {
this.$input.cache[this.doctype][this.df.fieldname] = {};
}
}
setup_awesomplete() {
this.awesomplete = new Awesomplete(
this.input,
@ -75,12 +106,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
.find('.awesomplete ul')
.css('min-width', '100%');
this.$input.on(
'input',
frappe.utils.debounce(() => {
this.init_option_cache();
this.$input.on('input', frappe.utils.debounce((e) => {
const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value];
if (cached_options && cached_options.length) {
this.set_data(cached_options);
} else if (this.get_query || this.df.get_query) {
this.execute_query_if_exists(e.target.value);
} else {
this.awesomplete.list = this.get_data();
}, 500)
);
}
}, 500));
this.$input.on('focus', () => {
if (!this.$input.val()) {
@ -89,6 +126,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
}
});
this.$input.on("blur", () => {
if(this.selected) {
this.selected = false;
return;
}
var value = this.get_input_value();
if(value!==this.last_value) {
this.parse_validate_and_set_in_model(value);
}
});
this.$input.on("awesomplete-open", () => {
this.autocomplete_open = true;
});
@ -127,6 +175,75 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
return options;
}
execute_query_if_exists(term) {
const args = { txt: term };
let get_query = this.get_query || this.df.get_query;
if (!get_query) {
return;
}
let set_nulls = function(obj) {
$.each(obj, function(key, value) {
if (value !== undefined) {
obj[key] = value;
}
});
return obj;
};
let process_query_object = function(obj) {
if (obj.query) {
args.query = obj.query;
}
if (obj.params) {
set_nulls(obj.params);
Object.assign(args, obj.params);
}
// turn off value translation
if (obj.translate_values !== undefined) {
this.translate_values = obj.translate_values;
}
};
if ($.isPlainObject(get_query)) {
process_query_object(get_query);
} else if (typeof get_query === "string") {
args.query = get_query;
} else {
// get_query by function
var q = get_query(
(this.frm && this.frm.doc) || this.doc,
this.doctype,
this.docname
);
if (typeof q === "string") {
// returns a string
args.query = q;
} else if ($.isPlainObject(q)) {
// returns an object
process_query_object(q);
}
}
if (args.query) {
frappe.call({
method: args.query,
args: args,
callback: ({ message }) => {
if(!this.$input.is(":focus")) {
return;
}
this.$input.cache[this.doctype][this.df.fieldname][term] = message;
this.set_data(message);
}
})
}
}
get_data() {
return this._data || [];
}

View file

@ -160,8 +160,10 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
return value;
}
get_df_options() {
let df_options = this.df.options;
if (!df_options) return {};
let options = {};
let df_options = this.df.options || '';
if (typeof df_options === 'string') {
try {
options = JSON.parse(df_options);

View file

@ -1102,13 +1102,13 @@ frappe.ui.form.Form = class FrappeForm {
let list_view = frappe.get_list_view(this.doctype);
if (list_view) {
filters = list_view.get_filters_for_args();
sort_field = list_view.sort_field;
sort_field = list_view.sort_by;
sort_order = list_view.sort_order;
} else {
let list_settings = frappe.get_user_settings(this.doctype)['List'];
if (list_settings) {
filters = list_settings.filters;
sort_field = list_settings.sort_field;
sort_field = list_settings.sort_by;
sort_order = list_settings.sort_order;
}
}
@ -1552,7 +1552,9 @@ frappe.ui.form.Form = class FrappeForm {
// update child doc
opts.child = locals[opts.child.doctype][opts.child.name];
var std_field_list = ["doctype"].concat(frappe.model.std_fields_list);
var std_field_list = ["doctype"]
.concat(frappe.model.std_fields_list)
.concat(frappe.model.child_table_field_list);
for (var key in r.message) {
if (std_field_list.indexOf(key)===-1) {
opts.child[key] = r.message[key];

View file

@ -21,6 +21,9 @@ frappe.form.formatters = {
}
return value==null ? "" : value;
},
Autocomplete: function(value) {
return __(frappe.form.formatters["Data"](value));
},
Select: function(value) {
return __(frappe.form.formatters["Data"](value));
},

View file

@ -746,7 +746,7 @@ export default class Grid {
var df = this.visible_columns[i][0];
var colsize = this.visible_columns[i][1];
if (colsize > 1 && colsize < 11
&& !in_list(frappe.model.std_fields_list, df.fieldname)) {
&& frappe.model.is_non_std_field(df.fieldname)) {
if (passes < 3 && ["Int", "Currency", "Float", "Check", "Percent"].indexOf(df.fieldtype) !== -1) {
// don't increase col size of these fields in first 3 passes

View file

@ -5,11 +5,7 @@ export default class GridRow {
this.on_grid_fields_dict = {};
this.on_grid_fields = [];
$.extend(this, opts);
if (this.doc && this.parent_df.options) {
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
this.docfields = docfields.length ? docfields : opts.docfields;
}
this.set_docfields();
this.columns = {};
this.columns_list = [];
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
@ -41,6 +37,22 @@ export default class GridRow {
this.set_data();
}
}
set_docfields(update=false) {
if (this.doc && this.parent_df.options) {
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
if (update) {
// to maintain references
this.docfields.forEach(df => {
Object.assign(df, docfields.find(d => d.fieldname === df.fieldname));
});
} else {
this.docfields = docfields;
}
}
}
set_data() {
this.wrapper.data({
"doc": this.doc
@ -148,6 +160,11 @@ export default class GridRow {
}, __('Move To'), 'Update');
}
refresh() {
// update docfields for new record
if (this.frm && this.doc && this.doc.__islocal) {
this.set_docfields(true);
}
if(this.frm && this.doc) {
this.doc = locals[this.doc.doctype][this.doc.name];
}
@ -166,21 +183,20 @@ export default class GridRow {
render_template() {
this.set_row_index();
if(this.row_display) {
if (this.row_display) {
this.row_display.remove();
}
// row index
if(this.doc) {
if(!this.row_index) {
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
}
if (!this.row_index) {
this.row_index = $(`<div class="template-row-index">${this.row_check_html}<span></span></div>`).appendTo(this.row);
}
if (this.doc) {
this.row_index.find('span').html(this.doc.idx);
}
this.row_display = $('<div class="row-data sortable-handle template-row">'+
+'</div>').appendTo(this.row)
this.row_display = $('<div class="row-data sortable-handle template-row"></div>').appendTo(this.row)
.html(frappe.render(this.grid.template, {
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
frm: this.frm,
@ -323,7 +339,7 @@ export default class GridRow {
</div>
<div class='control-input-wrapper selected-fields'>
</div>
<p class='help-box small text-muted hidden-xs'>
<p class='help-box small text-muted'>
<a class='add-new-fields text-muted'>
+ ${__('Add / Remove Columns')}
</a>
@ -403,18 +419,18 @@ export default class GridRow {
data-label='${docfield.label}' data-type='${docfield.fieldtype}'>
<div class='row'>
<div class='col-md-1'>
<div class='col-md-1' style='padding-top: 2px'>
<a style='cursor: grabbing;'>${frappe.utils.icon('drag', 'xs')}</a>
</div>
<div class='col-md-7' style='padding-left:0px;'>
<div class='col-md-7' style='padding-left:0px; padding-top:3px'>
${__(docfield.label)}
</div>
<div class='col-md-3' style='padding-left:0px;margin-top:-2px;' title='${__('Columns')}'>
<input class='form-control column-width input-xs text-right'
value='${docfield.columns || cint(d.columns)}'
data-fieldname='${docfield.fieldname}' style='background-color: #ffff; display: inline'>
data-fieldname='${docfield.fieldname}' style='background-color: var(--modal-bg); display: inline'>
</div>
<div class='col-md-1'>
<div class='col-md-1' style='padding-top: 3px'>
<a class='text-muted remove-field' data-fieldname='${docfield.fieldname}'>
<i class='fa fa-trash-o' aria-hidden='true'></i>
</a>

View file

@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout {
// remove previous color
this.message.removeClass(this.message_color);
}
this.message_color = (color && ['yellow', 'blue'].includes(color)) ? color : 'blue';
this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue';
if (html) {
if (html.substr(0, 1)!=='<') {
// wrap in a block
@ -554,19 +554,21 @@ frappe.ui.form.Layout = class Layout {
let has_dep = false;
for (let fkey in this.fields_list) {
let f = this.fields_list[fkey];
f.dependencies_clear = true;
const fields = this.fields_list.concat(this.tabs);
for (let fkey in fields) {
let f = fields[fkey];
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
has_dep = true;
break;
}
}
if (!has_dep) return;
// show / hide based on values
for (let i = this.fields_list.length - 1; i >= 0; i--) {
let f = this.fields_list[i];
for (let i = fields.length - 1; i >= 0; i--) {
let f = fields[i];
f.guardian_has_value = true;
if (f.df.depends_on) {
// evaluate guardian

View file

@ -1,6 +1,6 @@
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
constructor(opts) {
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label, columns */
Object.assign(this, opts);
this.for_select = this.doctype == "[Select]";
if (!this.for_select) {
@ -400,23 +400,22 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
return this.results.filter(res => checked_values.includes(res.name));
}
get_datatable_columns() {
if (this.get_query && this.get_query().query && this.columns) return this.columns;
if (Array.isArray(this.setters))
return ["name", ...this.setters.map(df => df.fieldname)];
return ["name", ...Object.keys(this.setters)];
}
make_list_row(result = {}) {
var me = this;
// Make a head row by default (if result not passed)
let head = Object.keys(result).length === 0;
let contents = ``;
let columns = ["name"];
if ($.isArray(this.setters)) {
for (let df of this.setters) {
columns.push(df.fieldname);
}
} else {
columns = columns.concat(Object.keys(this.setters));
}
columns.forEach(function (column) {
this.get_datatable_columns().forEach(function (column) {
contents += `<div class="list-item__content ellipsis">
${
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
@ -486,7 +485,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
get_filters_from_setters() {
let me = this;
let filters = this.get_query ? this.get_query().filters : {} || {};
let filters = (this.get_query ? this.get_query().filters : {}) || {};
let filter_fields = [];
if ($.isArray(this.setters)) {

View file

@ -40,7 +40,7 @@ export default class Tab {
hide = true;
}
hide && this.toggle(false);
this.toggle(!hide);
}
toggle(show) {

View file

@ -101,10 +101,8 @@ frappe.ui.form.Toolbar = class Toolbar {
return frappe.xcall("frappe.model.rename_doc.update_document_title", {
doctype,
docname,
new_name,
title_field,
old_title: this.frm.doc[title_field],
new_title,
name: new_name,
title: new_title,
merge
}).then(new_docname => {
if (new_name != docname) {

View file

@ -204,6 +204,11 @@ frappe.views.BaseList = class BaseList {
};
if (frappe.boot.desk_settings.view_switcher) {
/* @preserve
for translation, don't remove
__("List View") __("Report View") __("Dashboard View") __("Gantt View"),
__("Kanban View") __("Calendar View") __("Image View") __("Inbox View"),
__("Tree View") __("Map View") */
this.views_menu = this.page.add_custom_button_group(__('{0} View', [this.view_name]),
icon_map[this.view_name] || 'list');
this.views_list = new frappe.views.ListViewSelect({
@ -465,9 +470,14 @@ frappe.views.BaseList = class BaseList {
}
refresh() {
let args = this.get_call_args();
if (this.no_change(args)) {
// console.log('throttled');
return Promise.resolve();
}
this.freeze(true);
// fetch data from server
return frappe.call(this.get_call_args()).then((r) => {
return frappe.call(args).then((r) => {
// render
this.prepare_data(r);
this.toggle_result_area();
@ -482,6 +492,19 @@ frappe.views.BaseList = class BaseList {
});
}
no_change(args) {
// returns true if arguments are same for the last 3 seconds
// this helps in throttling if called from various sources
if (this.last_args && JSON.stringify(args) === this.last_args) {
return true;
}
this.last_args = JSON.stringify(args);
setTimeout(() => {
this.last_args = null;
}, 3000);
return false;
}
prepare_data(r) {
let data = r.message || {};

View file

@ -1483,7 +1483,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return [
filter[1],
"=",
JSON.stringify([filter[2], filter[3]]),
encodeURIComponent(JSON.stringify([filter[2], filter[3]])),
].join("");
})
.join("&");

View file

@ -144,7 +144,7 @@ $.extend(frappe.meta, {
get_doctype_for_field: function(doctype, key) {
var out = null;
if(in_list(frappe.model.std_fields_list, key)) {
if (in_list(frappe.model.std_fields_list, key)) {
// standard
out = doctype;
} else if(frappe.meta.has_field(doctype, key)) {
@ -152,7 +152,7 @@ $.extend(frappe.meta, {
out = doctype;
} else {
frappe.meta.get_table_fields(doctype).every(function(d) {
if(frappe.meta.has_field(d.options, key)) {
if (frappe.meta.has_field(d.options, key) || in_list(frappe.model.child_table_field_list, key)) {
out = d.options;
return false;
}

View file

@ -12,6 +12,8 @@ $.extend(frappe.model, {
std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by',
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus', 'idx'],
child_table_field_list: ['parent', 'parenttype', 'parentfield'],
core_doctypes_list: ['DocType', 'DocField', 'DocPerm', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Client Script'],
@ -83,7 +85,7 @@ $.extend(frappe.model, {
},
is_non_std_field: function(fieldname) {
return !frappe.model.std_fields_list.includes(fieldname);
return ![...frappe.model.std_fields_list, ...frappe.model.child_table_field_list].includes(fieldname);
},
get_std_field: function(fieldname, ignore=false) {

View file

@ -170,7 +170,7 @@ frappe.ui.FilterGroup = class {
validate_args(doctype, fieldname) {
if (doctype && fieldname
&& !frappe.meta.has_field(doctype, fieldname)
&& !frappe.model.std_fields_list.includes(fieldname)) {
&& frappe.model.is_non_std_field(fieldname)) {
frappe.msgprint({
message: __('Invalid filter: {0}', [fieldname.bold()]),
@ -293,7 +293,7 @@ frappe.ui.FilterGroup = class {
</div>
</div>
<hr class="divider"></hr>
<div class="filter-action-buttons">
<div class="filter-action-buttons mt-2">
<button class="text-muted add-filter btn btn-xs">
+ ${__('Add a Filter')}
</button>

View file

@ -73,7 +73,7 @@ frappe.ui.LinkPreview = class {
}
this.popover_timeout = setTimeout(() => {
if (this.popover) {
if (this.popover && this.popover.options) {
let new_content = this.get_popover_html(preview_data);
this.popover.options.content = new_content;
} else {

View file

@ -578,6 +578,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
args: {
report_name: this.report_name,
filters: filters,
is_tree: this.report_settings.tree,
parent_field: this.report_settings.parent_field
},
callback: resolve,
always: () => this.page.btn_secondary.prop('disabled', false)
@ -834,7 +836,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
let data = this.data;
let columns = this.columns.filter((col) => !col.hidden);
if (this.raw_data.add_total_row) {
if (this.raw_data.add_total_row && !this.report_settings.tree) {
data = data.slice();
data.splice(-1, 1);
}
@ -854,7 +856,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
treeView: this.tree_report,
layout: 'fixed',
cellHeight: 33,
showTotalRow: this.raw_data.add_total_row,
showTotalRow: this.raw_data.add_total_row && !this.report_settings.tree,
direction: frappe.utils.is_rtl() ? 'rtl' : 'ltr',
hooks: {
columnTotal: frappe.utils.report_column_total

View file

@ -651,7 +651,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
&& !df.is_virtual
&& !df.hidden
// not a standard field i.e., owner, modified_by, etc.
&& !frappe.model.std_fields_list.includes(df.fieldname))
&& frappe.model.is_non_std_field(df.fieldname))
return true;
return false;
}

View file

@ -94,7 +94,10 @@
.frappe-control[data-fieldtype='Color'] {
input {
padding-left: 40px;
padding-left: 38px;
}
.control-input {
position: relative;
}
.selected-color {
cursor: pointer;
@ -103,7 +106,7 @@
border-radius: 5px;
background-color: red;
position: absolute;
top: calc(50% + 1px);
top: 5px;
left: 8px;
content: ' ';
&.no-value {
@ -113,10 +116,9 @@
}
.like-disabled-input {
.color-value {
padding-left: 25px;
padding-left: 26px;
}
.selected-color {
top: 20%;
cursor: default;
}
}

View file

@ -54,7 +54,7 @@
}
.form-grid .grid-heading-row .template-row {
margin-left: 20px;
margin-left: 8px;
}
.form-grid .template-row {
@ -88,6 +88,17 @@
margin-top: 2px;
}
.template-row-index {
float: left;
margin-left: 15px;
margin-top: 8px;
margin-right: -20px;
span {
margin-left: 5px;
}
}
.editable-form .grid-static-col.bold {
font-weight: bold;
}
@ -192,7 +203,7 @@
margin-left: var(--margin-xs);
button {
height: 27px;
height: 24px;
}
}

View file

@ -225,6 +225,11 @@ body.modal-open[style^="padding-right"] {
}
}
// modal is xs (for grids)
.modal .hidden-xs {
display: none !important;
}
.dialog-assignment-row {
display: flex;
align-items: center;

View file

@ -58,7 +58,7 @@
}
.link-btn {
top: 6px;
top: 0px;
}
select {
@ -77,7 +77,7 @@
padding: 0;
border: var(--dt-focus-border-width) solid #9bccf8;
input {
input[type="text"] {
font-size: inherit;
height: 27px;

View file

@ -50,6 +50,10 @@
&:last-child {
padding-right: 0;
}
@include media-breakpoint-down(sm) {
padding: 0;
}
}
}

View file

@ -65,7 +65,7 @@ def publish_realtime(event=None, message=None, room=None,
if after_commit:
params = [event, message, room]
if not params in frappe.local.realtime_log:
if params not in frappe.local.realtime_log:
frappe.local.realtime_log.append(params)
else:
emit_via_redis(event, message, room)

View file

@ -164,6 +164,7 @@ def get_alert_dict(doc):
return alert_dict
def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False):
doc = frappe._dict(doc)
@ -171,7 +172,7 @@ def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False):
ref_name, doc.rule, None if apply_only_once else doc.user)
if log_exists:
return
return frappe.get_doc('Energy Point Log', log_exists)
new_log = frappe.new_doc('Energy Point Log')
new_log.reference_doctype = ref_doctype

View file

@ -55,9 +55,6 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
if not frappe.db:
frappe.connect()
# if not frappe.conf.get("db_name").startswith("test_"):
# raise Exception, 'db_name must start with "test_"'
# workaround! since there is no separate test db
frappe.clear_cache()
scheduler_disabled_by_user = frappe.utils.scheduler.is_scheduler_disabled()
@ -285,7 +282,7 @@ def make_test_records(doctype, verbose=0, force=False):
if options == "[Select]":
continue
if not options in frappe.local.test_objects:
if options not in frappe.local.test_objects:
frappe.local.test_objects[options] = []
make_test_records(options, verbose, force)
make_test_records_for_doctype(options, verbose, force)
@ -392,7 +389,7 @@ def make_test_objects(doctype, test_records=None, verbose=None, reset=False):
try:
d.run_method("before_test_insert")
d.insert()
d.insert(ignore_if_duplicate=True)
if docstatus == 1:
d.submit()
@ -425,7 +422,7 @@ def add_to_test_record_log(doctype):
'''Add `doctype` to site/.test_log
`.test_log` is a cache of all doctypes for which test records are created'''
test_record_log = get_test_record_log()
if not doctype in test_record_log:
if doctype not in test_record_log:
frappe.flags.test_record_log.append(doctype)
with open(frappe.get_site_path('.test_log'), 'w') as f:
f.write('\n'.join(filter(None, frappe.flags.test_record_log)))

View file

@ -72,11 +72,14 @@ class FrappeAPITestCase(unittest.TestCase):
@property
def sid(self) -> str:
if not getattr(self, "_sid", None):
r = self.post("/api/method/login", {
"usr": "Administrator",
"pwd": frappe.conf.admin_password or "admin",
})
self._sid = r.headers[2][1].split(";")[0].lstrip("sid=")
from frappe.auth import CookieManager, LoginManager
from frappe.utils import set_request
set_request(path="/")
frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager()
frappe.local.login_manager.login_as('Administrator')
self._sid = frappe.session.sid
return self._sid
@ -112,16 +115,6 @@ class TestResourceAPI(FrappeAPITestCase):
frappe.delete_doc_if_exists(cls.DOCTYPE, name)
frappe.db.commit()
def setUp(self):
# commit to ensure consistency in session (postgres CI randomly fails)
if frappe.conf.db_type == "postgres":
frappe.db.commit()
if self._testMethodName == "test_auth_cycle":
from frappe.core.doctype.user.user import generate_keys
generate_keys("Administrator")
frappe.db.commit()
def test_unauthorized_call(self):
# test 1: fetch documents without auth
response = requests.get(f"{self.RESOURCE_URL}/{self.DOCTYPE}")
@ -226,6 +219,12 @@ class TestResourceAPI(FrappeAPITestCase):
class TestMethodAPI(FrappeAPITestCase):
METHOD_PATH = "/api/method"
def setUp(self):
if self._testMethodName == "test_auth_cycle":
from frappe.core.doctype.user.user import generate_keys
generate_keys("Administrator")
frappe.db.commit()
def test_version(self):
# test 1: test for /api/method/version
response = self.get(f"{self.METHOD_PATH}/version")

View file

@ -3,25 +3,37 @@
# imports - standard imports
import gzip
import importlib
import json
import os
import shlex
import shutil
import subprocess
from typing import List
import unittest
from contextlib import contextmanager
from functools import wraps
from glob import glob
from typing import List, Optional
from unittest.case import skipIf
from unittest.mock import patch
# imports - third party imports
import click
from click.testing import CliRunner, Result
from click import Command
# imports - module imports
import frappe
import frappe.commands.site
import frappe.commands.utils
import frappe.recorder
from frappe.installer import add_to_installed_apps, remove_app
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
from frappe.utils.backups import fetch_latest_backups
# imports - third party imports
import click
_result: Optional[Result] = None
TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
CLI_CONTEXT = frappe._dict(sites=[TEST_SITE])
def clean(value) -> str:
@ -76,7 +88,61 @@ def exists_in_backup(doctypes: List, file: os.PathLike) -> bool:
return len(missing_doctypes) == 0
@contextmanager
def maintain_locals():
pre_site = frappe.local.site
pre_flags = frappe.local.flags.copy()
pre_db = frappe.local.db
try:
yield
finally:
post_site = getattr(frappe.local, "site", None)
if not post_site or post_site != pre_site:
frappe.init(site=pre_site)
frappe.local.db = pre_db
frappe.local.flags.update(pre_flags)
def pass_test_context(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(CLI_CONTEXT, *args, **kwargs)
return decorated_function
@contextmanager
def cli(cmd: Command, args: Optional[List] = None):
with maintain_locals():
global _result
patch_ctx = patch("frappe.commands.pass_context", pass_test_context)
_module = cmd.callback.__module__
_cmd = cmd.callback.__qualname__
__module = importlib.import_module(_module)
patch_ctx.start()
importlib.reload(__module)
click_cmd = getattr(__module, _cmd)
try:
_result = CliRunner().invoke(click_cmd, args=args)
_result.command = str(cmd)
yield _result
finally:
patch_ctx.stop()
__module = importlib.import_module(_module)
importlib.reload(__module)
importlib.invalidate_caches()
class BaseTestCommands(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.setup_test_site()
return super().setUpClass()
@classmethod
def execute(self, command, kwargs=None):
site = {"site": frappe.local.site}
cmd_input = None
@ -102,16 +168,48 @@ class BaseTestCommands(unittest.TestCase):
self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode)
@classmethod
def setup_test_site(cls):
cmd_config = {
"test_site": TEST_SITE,
"admin_password": frappe.conf.admin_password,
"root_login": frappe.conf.root_login,
"root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type,
}
if not os.path.exists(
os.path.join(TEST_SITE, "site_config.json")
):
cls.execute(
"bench new-site {test_site} --admin-password {admin_password} --db-type"
" {db_type}",
cmd_config,
)
def _formatMessage(self, msg, standardMsg):
output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)
if not hasattr(self, "command") and _result:
command = _result.command
stdout = _result.stdout_bytes.decode() if _result.stdout_bytes else None
stderr = _result.stderr_bytes.decode() if _result.stderr_bytes else None
returncode = _result.exit_code
else:
command = self.command
stdout = self.stdout
stderr = self.stderr
returncode = self.returncode
cmd_execution_summary = "\n".join([
"-" * 70,
"Last Command Execution Summary:",
"Command: {}".format(self.command) if self.command else "",
"Standard Output: {}".format(self.stdout) if self.stdout else "",
"Standard Error: {}".format(self.stderr) if self.stderr else "",
"Return Code: {}".format(self.returncode) if self.returncode else "",
"Command: {}".format(command) if command else "",
"Standard Output: {}".format(stdout) if stdout else "",
"Standard Error: {}".format(stderr) if stderr else "",
"Return Code: {}".format(returncode) if returncode else "",
]).strip()
return "{}\n\n{}".format(output, cmd_execution_summary)
@ -135,6 +233,7 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))
@unittest.skip
def test_restore(self):
# step 0: create a site to run the test on
global_config = {
@ -143,35 +242,30 @@ class TestCommands(BaseTestCommands):
"root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type,
}
site_data = {"another_site": f"{frappe.local.site}-restore.test", **global_config}
site_data = {"test_site": TEST_SITE, **global_config}
for key, value in global_config.items():
if value:
self.execute(f"bench set-config {key} {value} -g")
self.execute(
"bench new-site {another_site} --admin-password {admin_password} --db-type"
" {db_type}",
site_data,
)
# test 1: bench restore from full backup
self.execute("bench --site {another_site} backup --ignore-backup-conf", site_data)
self.execute("bench --site {test_site} backup --ignore-backup-conf", site_data)
self.execute(
"bench --site {another_site} execute frappe.utils.backups.fetch_latest_backups",
"bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups",
site_data,
)
site_data.update({"database": json.loads(self.stdout)["database"]})
self.execute("bench --site {another_site} restore {database}", site_data)
self.execute("bench --site {test_site} restore {database}", site_data)
# test 2: restore from partial backup
self.execute("bench --site {another_site} backup --exclude 'ToDo'", site_data)
self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
site_data.update({"kw": "\"{'partial':True}\""})
self.execute(
"bench --site {another_site} execute"
"bench --site {test_site} execute"
" frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
site_data,
)
site_data.update({"database": json.loads(self.stdout)["database"]})
self.execute("bench --site {another_site} restore {database}", site_data)
self.execute("bench --site {test_site} restore {database}", site_data)
self.assertEqual(self.returncode, 1)
def test_partial_restore(self):
@ -226,7 +320,8 @@ class TestCommands(BaseTestCommands):
def test_list_apps(self):
# test 1: sanity check for command
self.execute("bench --site all list-apps")
self.assertEqual(self.returncode, 0)
self.assertIsNotNone(self.returncode)
self.assertIsInstance(self.stdout or self.stderr, str)
# test 2: bare functionality for single site
self.execute("bench --site {site} list-apps")
@ -242,14 +337,12 @@ class TestCommands(BaseTestCommands):
self.assertSetEqual(list_apps, installed_apps)
# test 3: parse json format
self.execute("bench --site all list-apps --format json")
self.execute("bench --site {site} list-apps --format json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)
self.execute("bench --site {site} list-apps --format json")
self.assertIsInstance(json.loads(self.stdout), dict)
self.execute("bench --site {site} list-apps -f json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)
def test_show_config(self):
@ -358,7 +451,7 @@ class TestCommands(BaseTestCommands):
)
def test_bench_drop_site_should_archive_site(self):
# TODO: Make this test postgres compatible
site = 'test_site.localhost'
site = TEST_SITE
self.execute(
f"bench new-site {site} --force --verbose "
@ -585,3 +678,18 @@ class TestRemoveApp(unittest.TestCase):
# nothing to assert, if this fails rest of the test suite will crumble.
remove_app("frappe", dry_run=True, yes=True, no_backup=True)
class TestSiteMigration(BaseTestCommands):
def test_migrate_cli(self):
with cli(frappe.commands.site.migrate) as result:
self.assertTrue(TEST_SITE in result.stdout)
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)
class TestBenchBuild(BaseTestCommands):
def test_build_assets(self):
with cli(frappe.commands.utils.build) as result:
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)

View file

@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import unittest, frappe, re, email
from frappe.email.doctype.email_account.test_email_account import TestEmailAccount
from frappe.email.doctype.email_account.test_email_account import TestEmailAccount
test_dependencies = ['Email Account']
@ -176,7 +176,6 @@ class TestEmail(unittest.TestCase):
with open(frappe.get_app_path('frappe', 'tests', 'data', 'email_with_image.txt'), 'r') as raw:
messages = {
# append_to = ToDo
'"INBOX"': {
'latest_messages': [
raw.read()
@ -185,17 +184,20 @@ class TestEmail(unittest.TestCase):
2: 'UNSEEN'
},
'uid_list': [2]
}
}
}
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
changed_flag = False
if not email_account.enable_incoming:
if not email_account.enable_incoming:
email_account.enable_incoming = True
changed_flag = True
mails = TestEmailAccount.mocked_get_inbound_mails(email_account, messages)
# mails = email_account.get_inbound_mails(test_mails=[raw.read()])
# TODO: fix this flaky test! - 'IndexError: list index out of range' for `.process()` line
if not mails:
raise self.skipTest("No inbound mails found / Email Account wasn't patched properly")
communication = mails[0].process()
self.assertTrue(re.search('''<img[^>]*src=["']/private/files/rtco1.png[^>]*>''', communication.content))

View file

@ -1,17 +1,30 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import unittest, frappe
from frappe.core.doctype.user.user import generate_keys
from frappe.frappeclient import FrappeClient, FrappeException
from frappe.utils.data import get_url
import base64
import unittest
import requests
import base64
import frappe
from frappe.core.doctype.user.user import generate_keys
from frappe.frappeclient import AuthError, FrappeClient, FrappeException
from frappe.utils.data import get_url
class TestFrappeClient(unittest.TestCase):
PASSWORD = frappe.conf.admin_password or "admin"
@classmethod
def setUpClass(cls) -> None:
site_url = get_url()
try:
FrappeClient(site_url, "Administrator", cls.PASSWORD, verify=False)
except AuthError:
raise unittest.SkipTest(f"AuthError raised for {site_url} [usr=Administrator, pwd={cls.PASSWORD}]")
return super().setUpClass()
def test_insert_many(self):
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))})

View file

@ -14,9 +14,12 @@ from frappe.core.doctype.user_permission.user_permission import clear_user_permi
from frappe.desk.form.load import getdoc
from frappe.utils.data import now_datetime
from frappe.tests.test_utils import FrappeTestCase
test_dependencies = ['Blogger', 'Blog Post', "User", "Contact", "Salutation"]
class TestPermissions(unittest.TestCase):
class TestPermissions(FrappeTestCase):
def setUp(self):
frappe.clear_cache(doctype="Blog Post")
@ -221,7 +224,7 @@ class TestPermissions(unittest.TestCase):
# check that Document.owner cannot be changed
user.reload()
user.owner = frappe.db.get_value("User", {"name": ("!=", user.name)})
user.owner = "Guest"
self.assertRaises(frappe.CannotChangeConstantError, user.save)
def test_set_only_once(self):
@ -557,7 +560,6 @@ class TestPermissions(unittest.TestCase):
# Remove delete perm
update('Blog Post', 'Website Manager', 0, 'delete', 0)
frappe.clear_cache(doctype="Blog Post")
frappe.set_user("test2@example.com")

View file

@ -1,13 +1,40 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import unittest
from contextlib import contextmanager, redirect_stdout
from io import StringIO
from random import choice, sample
from typing import List
from unittest.mock import patch
import frappe
from frappe.utils import add_to_date, now
from frappe.exceptions import DoesNotExistError
from random import choice, sample
from frappe.exceptions import DoesNotExistError, ValidationError
from frappe.model.base_document import get_controller
from frappe.model.rename_doc import bulk_rename, get_fetch_fields, update_document_title, update_linked_doctypes
from frappe.modules.utils import get_doc_path
from frappe.utils import add_to_date, now
@contextmanager
def patch_db(endpoints: List[str] = None):
patched_endpoints = []
for point in endpoints:
x = patch(f"frappe.db.{point}", new=lambda: True)
patched_endpoints.append(x)
savepoint = "SAVEPOINT_for_test_bulk_rename"
frappe.db.savepoint(save_point=savepoint)
try:
for x in patched_endpoints:
x.start()
yield
finally:
for x in patched_endpoints:
x.stop()
frappe.db.rollback(save_point=savepoint)
class TestRenameDoc(unittest.TestCase):
@ -50,6 +77,11 @@ class TestRenameDoc(unittest.TestCase):
@classmethod
def tearDownClass(self):
"""Deleting data generated for the tests defined under TestRenameDoc"""
# delete_doc doesnt drop tables
# this is done to bypass inconsistencies in the db
frappe.delete_doc_if_exists("DocType", "Renamed Doc")
frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`")
# delete the documents created
for docname in self.available_documents:
frappe.delete_doc(self.test_doctype, docname)
@ -153,7 +185,55 @@ class TestRenameDoc(unittest.TestCase):
new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)
)
# delete_doc doesnt drop tables
# this is done to bypass inconsistencies in the db
frappe.delete_doc_if_exists("DocType", "Renamed Doc")
frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`")
def test_update_document_title_api(self):
test_doctype = "Module Def"
test_doc = frappe.get_doc({
"doctype": test_doctype,
"module_name": f"Test-test_update_document_title_api-{frappe.generate_hash()}",
"custom": True,
})
test_doc.insert(ignore_mandatory=True)
dt = test_doc.doctype
dn = test_doc.name
new_name = f"{dn}-new"
# pass invalid types to API
with self.assertRaises(ValidationError):
update_document_title(doctype=dt, docname=dn, title={}, name={"hack": "this"})
doc_before = frappe.get_doc(test_doctype, dn)
return_value = update_document_title(doctype=dt, docname=dn, new_name=new_name)
doc_after = frappe.get_doc(test_doctype, return_value)
doc_before_dict = doc_before.as_dict(no_nulls=True, no_default_fields=True)
doc_after_dict = doc_after.as_dict(no_nulls=True, no_default_fields=True)
doc_before_dict.pop("module_name")
doc_after_dict.pop("module_name")
self.assertEqual(new_name, return_value)
self.assertDictEqual(doc_before_dict, doc_after_dict)
self.assertEqual(doc_after.module_name, return_value)
test_doc.delete()
def test_bulk_rename(self):
input_data = [[x, f"{x}-new"] for x in self.available_documents]
with patch_db(["commit", "rollback"]), patch("frappe.enqueue") as enqueue:
message_log = bulk_rename(self.test_doctype, input_data, via_console=False)
self.assertEqual(len(message_log), len(self.available_documents))
self.assertIsInstance(message_log, list)
enqueue.assert_called_with(
'frappe.utils.global_search.rebuild_for_doctype', doctype=self.test_doctype,
)
def test_deprecated_utils(self):
stdout = StringIO()
with redirect_stdout(stdout), patch_db(["set_value"]):
get_fetch_fields("User", "ToDo", ["Activity Log"])
self.assertTrue("Function frappe.model.rename_doc.get_fetch_fields" in stdout.getvalue())
update_linked_doctypes("User", "ToDo", "str", "str")
self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue())

View file

@ -512,3 +512,14 @@ class TestLinkTitle(unittest.TestCase):
prop_setter.delete()
class FrappeTestCase(unittest.TestCase):
"""Base test class for Frappe tests."""
@classmethod
def setUpClass(cls) -> None:
frappe.db.commit()
return super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
frappe.db.rollback()
return super().tearDownClass()

View file

@ -135,7 +135,7 @@ def get_dict(fortype, name=None):
asset_key = fortype + ":" + (name or "-")
translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {}
if not asset_key in translation_assets:
if asset_key not in translation_assets:
messages = []
if fortype=="doctype":
messages = get_messages_from_doctype(name)
@ -576,13 +576,15 @@ def get_server_messages(app):
def get_messages_from_include_files(app_name=None):
"""Returns messages from js files included at time of boot like desk.min.js for desk and web"""
from frappe.utils.jinja_globals import bundled_asset
messages = []
app_include_js = frappe.get_hooks("app_include_js", app_name=app_name) or []
web_include_js = frappe.get_hooks("web_include_js", app_name=app_name) or []
include_js = app_include_js + web_include_js
for js_path in include_js:
relative_path = os.path.join(frappe.local.sites_path, js_path.lstrip('/'))
file_path = bundled_asset(js_path)
relative_path = os.path.join(frappe.local.sites_path, file_path.lstrip('/'))
messages_from_file = get_messages_from_file(relative_path)
messages.extend(messages_from_file)

View file

@ -1530,7 +1530,7 @@ Main Section,Hauptbereich,
Make use of longer keyboard patterns,Nutzen Sie mehr Tastaturmuster,
Manage Third Party Apps,Verwalten von Apps von Drittanbietern,
Mandatory Information missing:,Pflichtangaben fehlen:,
Mandatory field: set role for,Pflichtfeld: set Rolle für,
Mandatory field: set role for,Pflichtfeld: Rolle anwenden auf,
Mandatory field: {0},Pflichtfeld: {0},
"Mandatory fields required in table {0}, Row {1}","Pflichtfelder in der Tabelle erforderlich {0}, Reihe {1}",
Mandatory fields required in {0},Für {0} benötigte Pflichtfelder:,
@ -2268,7 +2268,7 @@ Set Permissions,Festlegen von Berechtigungen,
Set Permissions on Document Types and Roles,Berechtigungen für Dokumenttypen und Rollen setzen,
Set Property After Alert,Setzen Sie die Eigenschaft nach Alert,
Set Quantity,Anzahl festlegen,
Set Role For,Set Rolle für,
Set Role For,Rolle anwenden auf,
Set User Permissions,Nutzer-Berechtigungen setzen,
Set Value,Wert festlegen,
Set custom roles for page and report,Legen Sie benutzerdefinierte Rollen für Seite und Bericht,
@ -3732,7 +3732,6 @@ Dr,Soll,
Due Date,Fälligkeitsdatum,
Duplicate,Duplizieren,
Edit Profile,Profil bearbeiten,
Email,Email,
End Time,Endzeit,
Enter Value,Wert eingeben,
Entity Type,Entitätstyp,
@ -4184,7 +4183,7 @@ Phone Number,Telefonnummer,
Linked Documents,Verknüpfte Dokumente,
Account SID,Konto-SID,
Steps,Schritte,
email,Email,
email,E-Mail,
Component,Komponente,
Subtitle,Untertitel,
Global Defaults,Allgemeine Voreinstellungen,

1 A4 A4
1530 Make use of longer keyboard patterns Nutzen Sie mehr Tastaturmuster
1531 Manage Third Party Apps Verwalten von Apps von Drittanbietern
1532 Mandatory Information missing: Pflichtangaben fehlen:
1533 Mandatory field: set role for Pflichtfeld: set Rolle für Pflichtfeld: Rolle anwenden auf
1534 Mandatory field: {0} Pflichtfeld: {0}
1535 Mandatory fields required in table {0}, Row {1} Pflichtfelder in der Tabelle erforderlich {0}, Reihe {1}
1536 Mandatory fields required in {0} Für {0} benötigte Pflichtfelder:
2268 Set Permissions on Document Types and Roles Berechtigungen für Dokumenttypen und Rollen setzen
2269 Set Property After Alert Setzen Sie die Eigenschaft nach Alert
2270 Set Quantity Anzahl festlegen
2271 Set Role For Set Rolle für Rolle anwenden auf
2272 Set User Permissions Nutzer-Berechtigungen setzen
2273 Set Value Wert festlegen
2274 Set custom roles for page and report Legen Sie benutzerdefinierte Rollen für Seite und Bericht
3732 Due Date Fälligkeitsdatum
3733 Duplicate Duplizieren
3734 Edit Profile Profil bearbeiten
Email Email
3735 End Time Endzeit
3736 Enter Value Wert eingeben
3737 Entity Type Entitätstyp
4183 Linked Documents Verknüpfte Dokumente
4184 Account SID Konto-SID
4185 Steps Schritte
4186 email Email E-Mail
4187 Component Komponente
4188 Subtitle Untertitel
4189 Global Defaults Allgemeine Voreinstellungen

View file

@ -1,6 +1,7 @@
import os
import socket
import time
from functools import lru_cache
from uuid import uuid4
from collections import defaultdict
from typing import List
@ -20,18 +21,22 @@ from frappe.utils.redis_queue import RedisQueue
from frappe.utils.commands import log
common_site_config = frappe.get_file_json("common_site_config.json")
custom_workers_config = common_site_config.get("workers", {})
default_timeout = 300
queue_timeout = {
"default": default_timeout,
"short": default_timeout,
"long": 1500,
**{
worker: config.get("timeout", default_timeout)
for worker, config in custom_workers_config.items()
@lru_cache()
def get_queues_timeout():
common_site_config = frappe.get_conf()
custom_workers_config = common_site_config.get("workers", {})
default_timeout = 300
return {
"default": default_timeout,
"short": default_timeout,
"long": 1500,
**{
worker: config.get("timeout", default_timeout)
for worker, config in custom_workers_config.items()
}
}
}
redis_connection = None
@ -57,7 +62,7 @@ def enqueue(method, queue='default', timeout=None, event=None,
q = get_queue(queue, is_async=is_async)
if not timeout:
timeout = queue_timeout.get(queue) or 300
timeout = get_queues_timeout().get(queue) or 300
queue_args = {
"site": frappe.local.site,
"user": frappe.session.user,
@ -204,7 +209,7 @@ def get_jobs(site=None, queue=None, key='method'):
def get_queue_list(queue_list=None, build_queue_name=False):
'''Defines possible queues. Also wraps a given queue in a list after validating.'''
default_queue_list = list(queue_timeout)
default_queue_list = list(get_queues_timeout())
if queue_list:
if isinstance(queue_list, str):
queue_list = [queue_list]
@ -236,7 +241,7 @@ def get_queue(qtype, is_async=True):
def validate_queue(queue, default_queue_list=None):
if not default_queue_list:
default_queue_list = list(queue_timeout)
default_queue_list = list(get_queues_timeout())
if queue not in default_queue_list:
frappe.throw(_("Queue should be one of {0}").format(', '.join(default_queue_list)))
@ -296,7 +301,7 @@ def generate_qname(qtype: str) -> str:
def is_queue_accessible(qobj: Queue) -> bool:
"""Checks whether queue is relate to current bench or not.
"""
accessible_queues = [generate_qname(q) for q in list(queue_timeout)]
accessible_queues = [generate_qname(q) for q in list(get_queues_timeout())]
return qobj.name in accessible_queues
def enqueue_test_job():

View file

@ -653,7 +653,7 @@ def get_backup_path():
@frappe.whitelist()
def get_backup_encryption_key():
frappe.only("System Manager")
frappe.only_for("System Manager")
return frappe.conf.encryption_key
class Backup:

View file

@ -90,7 +90,7 @@ def install_basic_docs():
for d in install_docs:
try:
frappe.get_doc(d).insert()
frappe.get_doc(d).insert(ignore_if_duplicate=True)
except frappe.NameError:
pass

View file

@ -97,13 +97,10 @@ def get_jloader():
if not getattr(frappe.local, 'jloader', None):
from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader
if frappe.local.flags.in_setup_help:
apps = ['frappe']
else:
apps = frappe.get_hooks('template_apps')
if not apps:
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True)
apps.reverse()
apps = frappe.get_hooks('template_apps')
if not apps:
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True)
apps.reverse()
if "frappe" not in apps:
apps.append('frappe')
@ -124,15 +121,13 @@ def set_filters(jenv):
import frappe
from frappe.utils import cint, cstr, flt
jenv.filters["json"] = frappe.as_json
jenv.filters["len"] = len
jenv.filters["int"] = cint
jenv.filters["str"] = cstr
jenv.filters["flt"] = flt
if frappe.flags.in_setup_help:
return
jenv.filters.update({
"json": frappe.as_json,
"len": len,
"int": cint,
"str": cstr,
"flt": flt,
})
def get_jinja_hooks():
"""Returns a tuple of (methods, filters) each containing a dict of method name and method definition pair."""

View file

@ -1,212 +0,0 @@
# This code is original from jsmin by Douglas Crockford, it was translated to
# Python by Baruch Even. The original code had the following copyright and
# license.
#
# /* jsmin.c
# 2007-05-22
#
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# */
from io import StringIO
def jsmin(js):
ins = StringIO(js)
outs = StringIO()
JavascriptMinify().minify(ins, outs)
str = outs.getvalue()
if len(str) > 0 and str[0] == '\n':
str = str[1:]
return str
def isAlphanum(c):
"""return true if the character is a letter, digit, underscore,
dollar sign, or non-ASCII character.
"""
return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
(c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
class UnterminatedComment(Exception):
pass
class UnterminatedStringLiteral(Exception):
pass
class UnterminatedRegularExpression(Exception):
pass
class JavascriptMinify(object):
def _outA(self):
self.outstream.write(self.theA)
def _outB(self):
self.outstream.write(self.theB)
def _get(self):
"""return the next character from stdin. Watch out for lookahead. If
the character is a control character, translate it to a space or
linefeed.
"""
c = self.theLookahead
self.theLookahead = None
if c is None:
c = self.instream.read(1)
if c >= ' ' or c == '\n':
return c
if c == '': # EOF
return '\000'
if c == '\r':
return '\n'
return ' '
def _peek(self):
self.theLookahead = self._get()
return self.theLookahead
def _next(self):
"""get the next character, excluding comments. peek() is used to see
if an unescaped '/' is followed by a '/' or '*'.
"""
c = self._get()
if c == '/' and self.theA != '\\':
p = self._peek()
if p == '/':
c = self._get()
while c > '\n':
c = self._get()
return c
if p == '*':
c = self._get()
while 1:
c = self._get()
if c == '*':
if self._peek() == '/':
self._get()
return ' '
if c == '\000':
raise UnterminatedComment()
return c
def _action(self, action):
"""do something! What you do is determined by the argument:
1 Output A. Copy B to A. Get the next B.
2 Copy B to A. Get the next B. (Delete A).
3 Get the next B. (Delete B).
action treats a string as a single character. Wow!
action recognizes a regular expression if it is preceded by ( or , or =.
"""
if action <= 1:
self._outA()
if action <= 2:
self.theA = self.theB
if self.theA == "'" or self.theA == '"':
while 1:
self._outA()
self.theA = self._get()
if self.theA == self.theB:
break
if self.theA <= '\n':
raise UnterminatedStringLiteral()
if self.theA == '\\':
self._outA()
self.theA = self._get()
if action <= 3:
self.theB = self._next()
if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
self.theA == '=' or self.theA == ':' or
self.theA == '[' or self.theA == '?' or
self.theA == '!' or self.theA == '&' or
self.theA == '|' or self.theA == ';' or
self.theA == '{' or self.theA == '}' or
self.theA == '\n'):
self._outA()
self._outB()
while 1:
self.theA = self._get()
if self.theA == '/':
break
elif self.theA == '\\':
self._outA()
self.theA = self._get()
elif self.theA <= '\n':
raise UnterminatedRegularExpression()
self._outA()
self.theB = self._next()
def _jsmin(self):
"""Copy the input to the output, deleting the characters which are
insignificant to JavaScript. Comments will be removed. Tabs will be
replaced with spaces. Carriage returns will be replaced with linefeeds.
Most spaces and linefeeds will be removed.
"""
self.theA = '\n'
self._action(3)
while self.theA != '\000':
if self.theA == ' ':
if isAlphanum(self.theB):
self._action(1)
else:
self._action(2)
elif self.theA == '\n':
if self.theB in ['{', '[', '(', '+', '-']:
self._action(1)
elif self.theB == ' ':
self._action(3)
else:
if isAlphanum(self.theB):
self._action(1)
else:
self._action(2)
else:
if self.theB == ' ':
if isAlphanum(self.theA):
self._action(1)
else:
self._action(3)
elif self.theB == '\n':
if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
self._action(1)
else:
if isAlphanum(self.theA):
self._action(1)
else:
self._action(3)
else:
self._action(1)
def minify(self, instream, outstream):
self.instream = instream
self.outstream = outstream
self.theA = '\n'
self.theB = None
self.theLookahead = None
self._jsmin()
self.instream.close()

View file

@ -155,7 +155,7 @@ def read_options_from_html(html):
toggle_visible_pdf(soup)
# use regex instead of soup-parser
for attr in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size", "header-spacing", "orientation"):
for attr in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size", "header-spacing", "orientation", "page-width", "page-height"):
try:
pattern = re.compile(r"(\.print-format)([\S|\s][^}]*?)(" + str(attr) + r":)(.+)(mm;)")
match = pattern.findall(html)

View file

@ -154,7 +154,7 @@ class RedisWrapper(redis.Redis):
_name = self.make_key(name, shared=shared)
# set in local
if not _name in frappe.local.cache:
if _name not in frappe.local.cache:
frappe.local.cache[_name] = {}
frappe.local.cache[_name][key] = value
@ -173,7 +173,7 @@ class RedisWrapper(redis.Redis):
def hget(self, name, key, generator=None, shared=False):
_name = self.make_key(name, shared=shared)
if not _name in frappe.local.cache:
if _name not in frappe.local.cache:
frappe.local.cache[_name] = {}
if not key: return None

View file

@ -139,7 +139,21 @@ def get_safe_globals():
get_hooks=get_hooks,
enqueue=safe_enqueue,
sanitize_html=frappe.utils.sanitize_html,
log_error=frappe.log_error
log_error=frappe.log_error,
db = NamespaceDict(
get_list=frappe.get_list,
get_all=frappe.get_all,
get_value=frappe.db.get_value,
set_value=frappe.db.set_value,
get_single_value=frappe.db.get_single_value,
get_default=frappe.db.get_default,
exists=frappe.db.exists,
count=frappe.db.count,
escape=frappe.db.escape,
sql=read_sql,
commit=frappe.db.commit,
rollback=frappe.db.rollback,
),
),
FrappeClient=FrappeClient,
style=frappe._dict(
@ -155,29 +169,11 @@ def get_safe_globals():
dev_server=1 if frappe._dev_server else 0,
run_script=run_script,
is_job_queued=is_job_queued,
get_visible_columns=get_visible_columns,
)
add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception))
if not frappe.flags.in_setup_help:
out.get_visible_columns = get_visible_columns
out.frappe.date_format = date_format
out.frappe.time_format = time_format
out.frappe.db = NamespaceDict(
get_list=frappe.get_list,
get_all=frappe.get_all,
get_value=frappe.db.get_value,
set_value=frappe.db.set_value,
get_single_value=frappe.db.get_single_value,
get_default=frappe.db.get_default,
exists=frappe.db.exists,
count=frappe.db.count,
escape=frappe.db.escape,
sql=read_sql,
commit=frappe.db.commit,
rollback=frappe.db.rollback,
)
if frappe.response:
out.frappe.response = frappe.response

2
frappe/utils/user.py Executable file → Normal file
View file

@ -79,7 +79,7 @@ class UserPermissions:
for r in get_valid_perms():
dt = r['parent']
if not dt in self.perm_map:
if dt not in self.perm_map:
self.perm_map[dt] = {}
for k in frappe.permissions.rights:

View file

@ -226,7 +226,7 @@ def get_full_index(route=None, app=None):
# order as per index if present
for route, children in children_map.items():
if not route in pages:
if route not in pages:
# no parent (?)
continue