Merge branch 'develop' of https://github.com/frappe/frappe into force_listview_columns

This commit is contained in:
Himanshu Warekar 2020-01-14 10:29:07 +05:30
commit b779767d2d
41 changed files with 743 additions and 339 deletions

View file

@ -6,7 +6,7 @@ globals attached to frappe module
"""
from __future__ import unicode_literals, print_function
from six import iteritems, binary_type, text_type, string_types
from six import iteritems, binary_type, text_type, string_types, PY2
from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
from past.builtins import cmp
@ -19,7 +19,7 @@ from .utils.jinja import (get_jenv, get_template, render_template, get_email_fro
# Harmless for Python 3
# For Python 2 set default encoding to utf-8
if sys.version[0] == '2':
if PY2:
reload(sys)
sys.setdefaultencoding("utf-8")
@ -321,10 +321,19 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
return
if as_table and type(msg) in (list, tuple):
out.msg = '<table border="1px" style="border-collapse: collapse" cellpadding="2px">' + ''.join(['<tr>'+''.join(['<td>%s</td>' % c for c in r])+'</tr>' for r in msg]) + '</table>'
if flags.print_messages and out.msg:
print("Message: " + repr(out.msg).encode("utf-8"))
table_rows = ''
for row in msg:
table_row_data = ''
for data in row:
table_row_data += '<td>{}</td>'.format(data)
table_rows += '<tr>{}</tr>'.format(table_row_data)
out.message = '''<table class="table table-bordered"
style="margin: 0;">{}</table>'''.format(table_rows)
if flags.print_messages and out.message:
print("Message: " + repr(out.message).encode("utf-8"))
if title:
out.title = title
@ -363,7 +372,6 @@ def throw(msg, exc=ValidationError, title=None):
msgprint(msg, raise_exception=exc, title=title, indicator='red')
def emit_js(js, user=False, **kwargs):
from frappe.realtime import publish_realtime
if user == False:
user = session.user
publish_realtime('eval_js', js, user=user, **kwargs)
@ -767,8 +775,8 @@ def get_meta_module(doctype):
import frappe.modules
return frappe.modules.load_doctype_module(doctype)
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None,
for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
"""Delete a document. Calls `frappe.model.delete_doc.delete_doc`.
:param doctype: DocType of document to be delete.
@ -803,8 +811,14 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False):
import frappe.modules
return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions)
@whitelist()
def rename_doc(*args, **kwargs):
"""Rename a document. Calls `frappe.model.rename_doc.rename_doc`"""
"""
Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link"
Calls `frappe.model.rename_doc.rename_doc`
"""
kwargs.pop('ignore_permissions', None)
from frappe.model.rename_doc import rename_doc
return rename_doc(*args, **kwargs)
@ -814,11 +828,11 @@ def get_module(modulename):
def scrub(txt):
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
return txt.replace(' ','_').replace('-', '_').lower()
return txt.replace(' ', '_').replace('-', '_').lower()
def unscrub(txt):
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
return txt.replace('_',' ').replace('-', ' ').title()
return txt.replace('_', ' ').replace('-', ' ').title()
def get_module_path(module, *joins):
"""Get the path of the given module name.
@ -980,7 +994,8 @@ def setup_module_map():
if not (local.app_modules and local.module_app):
local.module_app, local.app_modules = {}, {}
for app in get_all_apps(True):
if app=="webnotes": app="frappe"
if app == "webnotes":
app = "frappe"
local.app_modules.setdefault(app, [])
for module in get_module_list(app):
module = scrub(module)
@ -999,7 +1014,10 @@ def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
if content:
content = frappe.utils.strip(content)
return [p.strip() for p in content.splitlines() if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))]
return [
p.strip() for p in content.splitlines()
if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))
]
else:
return []
@ -1161,8 +1179,8 @@ def compare(val1, condition, val2):
import frappe.utils
return frappe.utils.compare(val1, condition, val2)
def respond_as_web_page(title, html, success=None, http_status_code=None,
context=None, indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
def respond_as_web_page(title, html, success=None, http_status_code=None, context=None,
indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
width=None, template='message'):
"""Send response as a web page with a message rather than JSON. Used to show permission errors etc.
@ -1351,7 +1369,8 @@ def format(*args, **kwargs):
import frappe.utils.formatters
return frappe.utils.formatters.format_value(*args, **kwargs)
def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None, output = None, no_letterhead = 0, password=None):
def get_print(doctype=None, name=None, print_format=None, style=None,
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
"""Get Print Format for given document.
:param doctype: DocType of document.
@ -1382,7 +1401,8 @@ def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
else:
return html
def attach_print(doctype, name, file_name=None, print_format=None, style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
def attach_print(doctype, name, file_name=None, print_format=None,
style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
from frappe.utils import scrub_urls
if not file_name: file_name = name
@ -1398,16 +1418,28 @@ def attach_print(doctype, name, file_name=None, print_format=None, style=None, h
no_letterhead = not print_letterhead
kwargs = dict(
print_format=print_format,
style=style,
html=html,
doc=doc,
no_letterhead=no_letterhead,
password=password
)
content = ''
if int(print_settings.send_print_as_pdf or 0):
out = {
"fname": file_name + ".pdf",
"fcontent": get_print(doctype, name, print_format=print_format, style=style, html=html, as_pdf=True, doc=doc, no_letterhead=no_letterhead, password=password)
}
ext = ".pdf"
kwargs["as_pdf"] = True
content = get_print(doctype, name, **kwargs)
else:
out = {
"fname": file_name + ".html",
"fcontent": scrub_urls(get_print(doctype, name, print_format=print_format, style=style, html=html, doc=doc, no_letterhead=no_letterhead, password=password)).encode("utf-8")
}
ext = ".html"
content = scrub_urls(get_print(doctype, name, **kwargs)).encode('utf-8')
out = {
"fname": file_name + ext,
"fcontent": content
}
local.flags.ignore_print_permissions = False
#reset lang to original local lang
@ -1526,7 +1558,12 @@ def log_error(message=None, title=None):
method=title)).insert(ignore_permissions=True)
def get_desk_link(doctype, name):
return '<a href="#Form/{0}/{1}" style="font-weight: bold;">{2} {1}</a>'.format(doctype, name, _(doctype))
html = '<a href="#Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
return html.format(
doctype=doctype,
name=name,
doctype_local=_(doctype)
)
def bold(text):
return '<b>{0}</b>'.format(text)
@ -1545,10 +1582,9 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
if not eval_globals:
eval_globals = {}
eval_globals['__builtins__'] = {}
eval_globals.update(whitelisted_globals)
return eval(code, eval_globals, eval_locals)
def get_system_settings(key):
@ -1560,49 +1596,46 @@ def get_active_domains():
from frappe.core.doctype.domain_settings.domain_settings import get_active_domains
return get_active_domains()
def get_version(doctype, name, limit = None, head = False, raise_err = True):
def get_version(doctype, name, limit=None, head=False, raise_err=True):
'''
Returns a list of version information of a given DocType (Applicable only if DocType has changes tracked).
Returns a list of version information of a given DocType.
Note: Applicable only if DocType has changes tracked.
Example
>>> frappe.get_version('User', 'foobar@gmail.com')
>>>
[
{
"version": [version.data], # Refer Version DocType get_diff method and data attribute
"user": "admin@gmail.com" # User that created this version
"creation": <datetime.datetime> # Creation timestamp of that object.
"version": [version.data], # Refer Version DocType get_diff method and data attribute
"user": "admin@gmail.com", # User that created this version
"creation": <datetime.datetime> # Creation timestamp of that object.
}
]
'''
meta = get_meta(doctype)
meta = get_meta(doctype)
if meta.track_changes:
names = db.sql("""
SELECT name from tabVersion
WHERE ref_doctype = '{doctype}' AND docname = '{name}'
{order_by}
{limit}
""".format(
doctype = doctype,
name = name,
order_by = 'ORDER BY creation' if head else '',
limit = 'LIMIT {limit}'.format(limit = limit) if limit else ''
))
names = db.get_all('Version', filters={
'ref_doctype': doctype,
'docname': name,
'order_by': 'creation' if head else None,
'limit': limit
}, as_list=1)
from frappe.chat.util import squashify, dictify, safe_json_loads
versions = [ ]
versions = []
for name in names:
name = squashify(name)
doc = get_doc('Version', name)
doc = get_doc('Version', name)
data = doc.data
data = safe_json_loads(data)
data = dictify(dict(
version = data,
user = doc.owner,
creation = doc.creation
version=data,
user=doc.owner,
creation=doc.creation
))
versions.append(data)
@ -1610,16 +1643,14 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True):
return versions
else:
if raise_err:
raise ValueError('{doctype} has no versions tracked.'.format(
doctype = doctype
))
raise ValueError(_('{0} has no versions tracked.').format(doctype))
@whitelist(allow_guest = True)
@whitelist(allow_guest=True)
def ping():
return "pong"
def safe_encode(param, encoding = 'utf-8'):
def safe_encode(param, encoding='utf-8'):
try:
param = param.encode(encoding)
except Exception:
@ -1627,7 +1658,7 @@ def safe_encode(param, encoding = 'utf-8'):
return param
def safe_decode(param, encoding = 'utf-8'):
def safe_decode(param, encoding='utf-8'):
try:
param = param.decode(encoding)
except Exception:
@ -1638,9 +1669,9 @@ def parse_json(val):
from frappe.utils import parse_json
return parse_json(val)
def mock(type, size = 1, locale = 'en'):
results = [ ]
faker = Faker(locale)
def mock(type, size=1, locale='en'):
results = []
faker = Faker(locale)
if not type in dir(faker):
raise ValueError('Not a valid mock type.')
else:
@ -1649,7 +1680,4 @@ def mock(type, size = 1, locale = 'en'):
results.append(data)
from frappe.chat.util import squashify
results = squashify(results)
return results
return squashify(results)

View file

@ -1,16 +1,24 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
import os, frappe, json, shutil, re, warnings, tempfile
from os.path import exists as path_exists, join as join_path, abspath, isdir
from __future__ import print_function, unicode_literals
import os
import re
import json
import shutil
import warnings
import tempfile
from distutils.spawn import find_executable
from six import iteritems, text_type
import frappe
from frappe.utils.minify import JavascriptMinify
"""
Build the `public` folders and setup languages
"""
timestamps = {}
app_paths = None
def symlink(target, link_name, overwrite=False):
@ -23,8 +31,7 @@ def symlink(target, link_name, overwrite=False):
'''
if not overwrite:
os.symlink(target, linkname)
return
return os.symlink(target, link_name)
# os.replace() may fail if files are on different filesystems
link_dir = os.path.dirname(link_name)
@ -57,16 +64,17 @@ def symlink(target, link_name, overwrite=False):
raise
app_paths = None
def setup():
global app_paths
pymodules = []
for app in frappe.get_all_apps(True):
try:
pymodules.append(frappe.get_module(app))
except ImportError: pass
except ImportError:
pass
app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules]
def get_node_pacman():
pacmans = ['yarn', 'npm']
for exec_ in pacmans:
@ -75,6 +83,7 @@ def get_node_pacman():
return exec_
raise ValueError('No Node.js Package Manager found.')
def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False):
"""concat / minify js files"""
setup()
@ -87,77 +96,77 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False)
if app:
command += ' --app {app}'.format(app=app)
frappe_app_path = abspath(join_path(app_paths[0], '..'))
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
check_yarn()
frappe.commands.popen(command, cwd=frappe_app_path)
def watch(no_compress):
"""watch and rebuild if necessary"""
setup()
pacman = get_node_pacman()
frappe_app_path = abspath(join_path(app_paths[0], '..'))
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
check_yarn()
frappe_app_path = frappe.get_app_path('frappe', '..')
frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd = frappe_app_path)
frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd=frappe_app_path)
def check_yarn():
from distutils.spawn import find_executable
if not find_executable('yarn'):
print('Please install yarn using below command and try again.')
print('npm install -g yarn')
return
print('Please install yarn using below command and try again.\nnpm install -g yarn')
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.
assets_path = join_path(frappe.local.sites_path, "assets")
for dir_path in [
join_path(assets_path, 'js'),
join_path(assets_path, 'css')]:
assets_path = os.path.join(frappe.local.sites_path, "assets")
if not path_exists(dir_path):
for dir_path in [os.path.join(assets_path, 'js'), os.path.join(assets_path, 'css')]:
if not os.path.exists(dir_path):
os.makedirs(dir_path)
for app_name in frappe.get_all_apps(True):
pymodule = frappe.get_module(app_name)
app_base_path = abspath(os.path.dirname(pymodule.__file__))
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
symlinks = []
app_public_path = join_path(app_base_path, 'public')
app_public_path = os.path.join(app_base_path, 'public')
# app/public > assets/app
symlinks.append([app_public_path, join_path(assets_path, app_name)])
symlinks.append([app_public_path, os.path.join(assets_path, app_name)])
# app/node_modules > assets/app/node_modules
if path_exists(abspath(app_public_path)):
symlinks.append([join_path(app_base_path, '..', 'node_modules'), join_path(assets_path, app_name, 'node_modules')])
if os.path.exists(os.path.abspath(app_public_path)):
symlinks.append([os.path.join(app_base_path, '..', 'node_modules'), os.path.join(
assets_path, app_name, 'node_modules')])
app_doc_path = None
if isdir(join_path(app_base_path, 'docs')):
app_doc_path = join_path(app_base_path, 'docs')
if os.path.isdir(os.path.join(app_base_path, 'docs')):
app_doc_path = os.path.join(app_base_path, 'docs')
elif isdir(join_path(app_base_path, 'www', 'docs')):
app_doc_path = join_path(app_base_path, 'www', 'docs')
elif os.path.isdir(os.path.join(app_base_path, 'www', 'docs')):
app_doc_path = os.path.join(app_base_path, 'www', 'docs')
if app_doc_path:
symlinks.append([app_doc_path, join_path(assets_path, app_name + '_docs')])
symlinks.append([app_doc_path, os.path.join(
assets_path, app_name + '_docs')])
for source, target in symlinks:
source = abspath(source)
if path_exists(source):
source = os.path.abspath(source)
if os.path.exists(source):
if restore:
if path_exists(target):
if os.path.exists(target):
if os.path.islink(target):
os.unlink(target)
else:
shutil.rmtree(target)
shutil.copytree(source, target)
elif make_copy:
if path_exists(target):
warnings.warn('Target {target} already exists.'.format(target = target))
if os.path.exists(target):
warnings.warn('Target {target} already exists.'.format(target=target))
else:
shutil.copytree(source, target)
else:
if path_exists(target):
if os.path.exists(target):
if os.path.islink(target):
os.unlink(target)
else:
@ -170,11 +179,13 @@ def make_asset_dirs(make_copy=False, restore=False):
# warnings.warn('Source {source} does not exist.'.format(source = source))
pass
def build(no_compress=False, verbose=False):
assets_path = join_path(frappe.local.sites_path, "assets")
assets_path = os.path.join(frappe.local.sites_path, "assets")
for target, sources in iteritems(get_build_maps()):
pack(join_path(assets_path, target), sources, no_compress, verbose)
pack(os.path.join(assets_path, target), sources, no_compress, verbose)
def get_build_maps():
"""get all build.jsons with absolute paths"""
@ -182,8 +193,8 @@ def get_build_maps():
build_maps = {}
for app_path in app_paths:
path = join_path(app_path, 'public', 'build.json')
if path_exists(path):
path = os.path.join(app_path, 'public', 'build.json')
if os.path.exists(path):
with open(path) as f:
try:
for target, sources in iteritems(json.loads(f.read())):
@ -191,9 +202,10 @@ def get_build_maps():
source_paths = []
for source in sources:
if isinstance(source, list):
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
s = frappe.get_pymodule_path(
source[0], *source[1].split("/"))
else:
s = join_path(app_path, source)
s = os.path.join(app_path, source)
source_paths.append(s)
build_maps[target] = source_paths
@ -202,7 +214,6 @@ def get_build_maps():
print('JSON syntax error {0}'.format(str(e)))
return build_maps
timestamps = {}
def pack(target, sources, no_compress, verbose):
from six import StringIO
@ -212,8 +223,9 @@ def pack(target, sources, no_compress, verbose):
for f in sources:
suffix = None
if ':' in f: f, suffix = f.split(':')
if not path_exists(f) or isdir(f):
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)
@ -223,7 +235,7 @@ def pack(target, sources, no_compress, verbose):
extn = f.rsplit(".", 1)[1]
if outtype=="js" and extn=="js" and (not no_compress) and suffix!="concat" and (".min." not in f):
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()
@ -232,7 +244,7 @@ def pack(target, sources, no_compress, verbose):
if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
elif outtype=="js" and extn=="html":
elif outtype == "js" and extn == "html":
# add to frappe.templates
outtxt += html_to_js_template(f, data)
else:
@ -248,43 +260,48 @@ def pack(target, sources, no_compress, verbose):
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(\
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
content = re.sub("\s+", " ", content)
# strip comments
content = re.sub("(<!--.*?-->)", "", content)
content = re.sub("(<!--.*?-->)", "", content)
return content.replace("'", "\'")
def files_dirty():
for target, sources in iteritems(get_build_maps()):
for f in sources:
if ':' in f: f, suffix = f.split(':')
if not path_exists(f) or isdir(f): continue
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():
from distutils.spawn import find_executable
if not find_executable("lessc"):
return
for path in app_paths:
less_path = join_path(path, "public", "less")
if path_exists(less_path):
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 = join_path(less_path, fname)
fpath = os.path.join(less_path, fname)
mtime = os.path.getmtime(fpath)
if fpath in timestamps and mtime == timestamps[fpath]:
continue
@ -293,5 +310,5 @@ def compile_less():
print("compiling {0}".format(fpath))
css_path = join_path(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
os.system("lessc {0} > {1}".format(fpath, css_path))

View file

@ -12,6 +12,7 @@ from coverage import Coverage
import cProfile, pstats
from six import StringIO
@click.command('build')
@click.option('--app', help='Build assets for app')
@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking')
@ -26,15 +27,15 @@ def build(app=None, make_copy=False, restore = False, verbose=False):
no_compress = frappe.local.conf.developer_mode or False
frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore = restore, verbose=verbose)
@click.command('watch')
def watch():
"Watch and concatenate JS and CSS files as and when they change"
# if os.environ.get('CI'):
# return
import frappe.build
frappe.init('')
frappe.build.watch(True)
@click.command('clear-cache')
@pass_context
def clear_cache(context):
@ -51,6 +52,7 @@ def clear_cache(context):
finally:
frappe.destroy()
@click.command('clear-website-cache')
@pass_context
def clear_website_cache(context):
@ -64,6 +66,7 @@ def clear_website_cache(context):
finally:
frappe.destroy()
@click.command('destroy-all-sessions')
@click.option('--reason')
@pass_context
@ -79,6 +82,7 @@ def destroy_all_sessions(context, reason=None):
finally:
frappe.destroy()
@click.command('show-config')
@pass_context
def show_config(context):
@ -89,6 +93,7 @@ def show_config(context):
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path)
print_config(configuration)
def print_config(config):
for conf, value in config.items():
if isinstance(value, dict):
@ -96,6 +101,7 @@ def print_config(config):
else:
print("\t{:<50} {:<15}".format(conf, value))
@click.command('reset-perms')
@pass_context
def reset_perms(context):
@ -112,6 +118,7 @@ def reset_perms(context):
finally:
frappe.destroy()
@click.command('execute')
@click.argument('method')
@click.option('--args')
@ -191,6 +198,7 @@ def export_doc(context, doctype, docname):
finally:
frappe.destroy()
@click.command('export-json')
@click.argument('doctype')
@click.argument('path')
@ -207,6 +215,7 @@ def export_json(context, doctype, path, name=None):
finally:
frappe.destroy()
@click.command('export-csv')
@click.argument('doctype')
@click.argument('path')
@ -222,6 +231,7 @@ def export_csv(context, doctype, path):
finally:
frappe.destroy()
@click.command('export-fixtures')
@click.option('--app', default=None, help='Export fixtures of a specific app')
@pass_context
@ -236,6 +246,7 @@ def export_fixtures(context, app=None):
finally:
frappe.destroy()
@click.command('import-doc')
@click.argument('path')
@pass_context
@ -257,6 +268,7 @@ def import_doc(context, path, force=False):
finally:
frappe.destroy()
@click.command('import-csv')
@click.argument('path')
@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records')
@ -264,6 +276,7 @@ def import_doc(context, path, force=False):
@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode')
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
"Import CSV using data import"
@ -324,7 +337,7 @@ def data_import(context, file_path, doctype, import_type=None, submit_after_impo
@click.argument('doctype')
@click.argument('path')
@pass_context
def _bulk_rename(context, doctype, path):
def bulk_rename(context, doctype, path):
"Rename multiple records via CSV file"
from frappe.model.rename_doc import bulk_rename
from frappe.utils.csvutils import read_csv_content
@ -341,15 +354,6 @@ def _bulk_rename(context, doctype, path):
frappe.destroy()
@click.command('mysql')
def mysql():
"""
Deprecated
"""
click.echo("""
mysql command is deprecated.
Did you mean "bench mariadb"?
""")
@click.command('mariadb')
@pass_context
@ -374,6 +378,7 @@ def mariadb(context):
'--safe-updates',
"-A"])
@click.command('postgres')
@pass_context
def postgres(context):
@ -386,22 +391,21 @@ def postgres(context):
psql = find_executable('psql')
subprocess.run([ psql, '-d', frappe.conf.db_name])
@click.command('jupyter')
@pass_context
def jupyter(context):
try:
from pip import main
except ImportError:
from pip._internal import main
installed_packages = (r.split('==')[0] for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'], encoding='utf8'))
reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'])
installed_packages = [r.decode().split('==')[0] for r in reqs.split()]
if 'jupyter' not in installed_packages:
main(['install', 'jupyter'])
subprocess.check_output([sys.executable, '-m', 'pip', 'install', 'jupyter'])
site = get_site(context)
frappe.init(site=site)
jupyter_notebooks_path = os.path.abspath(frappe.get_site_path('jupyter_notebooks'))
sites_path = os.path.abspath(frappe.get_site_path('..'))
try:
os.stat(jupyter_notebooks_path)
except OSError:
@ -425,6 +429,7 @@ frappe.db.connect()
jupyter_notebooks_path,
])
@click.command('console')
@pass_context
def console(context):
@ -434,7 +439,12 @@ def console(context):
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
import IPython
IPython.embed(display_banner = "")
all_apps = frappe.get_installed_apps()
for app in all_apps:
locals()[app] = __import__(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
IPython.embed(display_banner="", header="")
@click.command('run-tests')
@click.option('--app', help="For App")
@ -481,8 +491,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
ui_tests = ui_tests, doctype_list_path = doctype_list_path, failfast=failfast)
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
if coverage:
cov.stop()
@ -494,6 +504,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
if os.environ.get('CI'):
sys.exit(ret)
@click.command('run-ui-tests')
@click.argument('app')
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
@ -516,6 +527,7 @@ def run_ui_tests(context, app, headless=False):
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@click.command('serve')
@click.option('--port', default=8000)
@click.option('--profile', is_flag=True, default=False)
@ -533,6 +545,7 @@ def serve(context, port=None, profile=False, no_reload=False, no_threading=False
frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
@click.command('request')
@click.option('--args', help='arguments like `?cmd=test&key=value` or `/api/request/method?..`')
@click.option('--path', help='path to request JSON')
@ -565,6 +578,7 @@ def request(context, args=None, path=None):
finally:
frappe.destroy()
@click.command('make-app')
@click.argument('destination')
@click.argument('app_name')
@ -573,6 +587,7 @@ def make_app(destination, app_name):
from frappe.utils.boilerplate import make_boilerplate
make_boilerplate(destination, app_name)
@click.command('set-config')
@click.argument('key')
@click.argument('value')
@ -596,6 +611,7 @@ def set_config(context, key, value, global_ = False, as_dict=False):
update_site_config(key, value, validate=False)
frappe.destroy()
@click.command('version')
def get_version():
"Show the versions of all the installed apps"
@ -614,32 +630,6 @@ def get_version():
print("{0} {1}".format(m, module.__version__))
@click.command('setup-global-help')
@click.option('--db_type')
@click.option('--root_password')
def setup_global_help(db_type=None, root_password=None):
'''Deprecated: setup help table in a separate database that will be
shared by the whole bench and set `global_help_setup` as 1 in
common_site_config.json'''
print_in_app_help_deprecation()
@click.command('get-docs-app')
@click.argument('app')
def get_docs_app(app):
'''Deprecated: Get the docs app for given app'''
print_in_app_help_deprecation()
@click.command('get-all-docs-apps')
def get_all_docs_apps():
'''Deprecated: Get docs apps for all apps'''
print_in_app_help_deprecation()
@click.command('setup-help')
@pass_context
def setup_help(context):
'''Deprecated: Setup help table in the current site (called after migrate)'''
print_in_app_help_deprecation()
@click.command('rebuild-global-search')
@click.option('--static-pages', is_flag=True, default=False, help='Rebuild global search for static pages')
@pass_context
@ -669,6 +659,7 @@ def rebuild_global_search(context, static_pages=False):
finally:
frappe.destroy()
@click.command('auto-deploy')
@click.argument('app')
@click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling')
@ -713,9 +704,6 @@ def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'):
else:
print('No Updates')
def print_in_app_help_deprecation():
print("In app help has been removed.\nYou can access the documentation on erpnext.com/docs or frappe.io/docs")
return
commands = [
build,
@ -734,7 +722,6 @@ commands = [
data_import,
import_doc,
make_app,
mysql,
mariadb,
postgres,
request,
@ -745,9 +732,7 @@ commands = [
set_config,
show_config,
watch,
_bulk_rename,
bulk_rename,
add_to_email_queue,
setup_global_help,
setup_help,
rebuild_global_search
]

View file

@ -105,7 +105,7 @@ def get_data():
"label": _('Leaderboard'),
"icon": "fa fa-trophy",
"type": 'link',
"link": '#social/users',
"link": '#leaderboard/User',
"color": '#FF4136',
'standard': 1,
},

View file

@ -351,27 +351,25 @@ class Importer:
return value
def parse_date_format(self, value, df):
date_format = self.guess_date_format_for_column(df.fieldname)
date_format = self.guess_date_format_for_column(df)
if date_format:
return datetime.strptime(value, date_format)
return value
def guess_date_format_for_column(self, fieldname):
def guess_date_format_for_column(self, df):
""" Guesses date format for a column by parsing the first 10 values in the column,
getting the date format and then returning the one which has the maximum frequency
"""
PARSE_ROW_COUNT = 10
if not self._guessed_date_formats.get(fieldname):
column_index = -1
if not self._guessed_date_formats.get(df.fieldname):
matches = [col for col in self.columns if col.df == df]
if not matches:
self._guessed_date_formats[df.fieldname] = None
return
for i, field in enumerate(self.header_row):
if self.meta.has_field(field) and field == fieldname:
column_index = i
break
if column_index == -1:
self._guessed_date_formats[fieldname] = None
column = matches[0]
column_index = column.index - 1
date_values = [
row[column_index] for row in self.data[:PARSE_ROW_COUNT] if row[column_index]
@ -380,9 +378,9 @@ class Importer:
if not date_formats:
return
max_occurred_date_format = max(set(date_formats), key=date_formats.count)
self._guessed_date_formats[fieldname] = max_occurred_date_format
self._guessed_date_formats[df.fieldname] = max_occurred_date_format
return self._guessed_date_formats[fieldname]
return self._guessed_date_formats[df.fieldname]
def import_data(self):
# set user lang for translations

View file

@ -26,7 +26,7 @@ class TestDocType(unittest.TestCase):
}],
"permissions": [{
"role": "System Manager",
"read": 1
"read": 1,
}],
"name": name
})
@ -295,3 +295,58 @@ class TestDocType(unittest.TestCase):
field_1.search_index = 1
self.assertRaises(CannotIndexedError, doc.insert)
def test_cancel_link_doctype(self):
import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create doctype
link_doc = self.new_doctype('Test Linked Doctype')
link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
link_doc.insert()
doc = self.new_doctype('Test Doctype')
doc.is_submittable = 1
field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype'
field_2.fieldname = 'test_linked_doctype'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype'
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
doc.insert()
# create doctype data
data_link_doc = frappe.new_doc('Test Linked Doctype')
data_link_doc.some_fieldname = 'Data1'
data_link_doc.insert()
data_link_doc.save()
data_link_doc.submit()
data_doc = frappe.new_doc('Test Doctype')
data_doc.some_fieldname = 'Data1'
data_doc.test_linked_doctype = data_link_doc.name
data_doc.insert()
data_doc.save()
data_doc.submit()
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs)
data_link_doc.cancel()
data_doc.load_from_db()
self.assertEqual(data_link_doc.docstatus, 2)
self.assertEqual(data_doc.docstatus, 2)
# delete doctype record
data_doc.delete()
data_link_doc.delete()
# delete doctype
link_doc.delete()
doc.delete()
frappe.db.commit()

View file

@ -517,11 +517,7 @@ class File(Document):
delete_file(self.thumbnail_url)
def is_downloadable(self):
if self.is_private:
if has_permission(self, 'read'):
return True
return False
return self.is_private and has_permission(self, 'read')
def get_extension(self):
'''returns split filename and extension'''
@ -587,7 +583,8 @@ def setup_folder_path(filename, new_parent):
file.save()
if file.is_folder:
frappe.rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True)
from frappe.model.rename_doc import rename_doc
rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True)
def get_extension(filename, extn, content):
mimetype = None

View file

@ -171,7 +171,6 @@ def get_documents_for_tag(tag):
"content": res.title
})
print(results)
return results
@frappe.whitelist()

View file

@ -1,14 +1,119 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, json
import json
from collections import defaultdict
from six import string_types
import frappe
import frappe.desk.form.load
import frappe.desk.form.meta
from frappe import _
from frappe.model.meta import is_single
from frappe.modules import load_doctype_module
import frappe.desk.form.meta
import frappe.desk.form.load
from six import string_types
from collections import defaultdict
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None):
"""
Get all nested submitted linked doctype linkinfo
Arguments:
doctype (str) - The doctype for which get all linked doctypes
name (str) - The docname for which get all linked doctypes
Keyword Arguments:
docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
Returns:
dict - Return list of documents and link count
"""
if not docs:
docs = []
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
for link_doctype, link_names in linked_docs.items():
for link in link_names:
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
if not validated_doc:
continue
link_count += 1
if link.name in [doc.get("name") for doc in docs]:
continue
links = get_submitted_linked_docs(link_doctype, link.name, docs)
docs.append({
"doctype": link_doctype,
"name": link.name,
"docstatus": link.docstatus,
"link_count": links.get("count")
})
# sort linked documents by ascending number of links
docs.sort(key=lambda doc: doc.get("link_count"))
return {
"docs": docs,
"count": link_count
}
@frappe.whitelist()
def cancel_all_linked_docs(docs):
"""
Cancel all linked doctype
Arguments:
docs (str) - It contains all list of dictionaries of a linked documents.
"""
docs = json.loads(docs)
for i, doc in enumerate(docs, 1):
if validate_linked_doc(doc) is True:
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
linked_doc.cancel()
def validate_linked_doc(docinfo):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
Args:
docs (dict): The document to check for submitted and non-exempt from auto-cancel
Returns:
bool: True if linked document passes all validations, else False
"""
# skip non-submittable doctypes since they don't need to be cancelled
if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
return False
# skip draft or cancelled documents
if docinfo.get('docstatus') != 1:
return False
# skip other doctypes since they don't need to be cancelled
auto_cancel_exempt_doctypes = get_exempted_doctypes()
if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
return False
return True
def get_exempted_doctypes():
""" Get list of doctypes exempted from being auto-cancelled """
auto_cancel_exempt_doctypes = []
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
auto_cancel_exempt_doctypes.append(doctypes)
return auto_cancel_exempt_doctypes
@frappe.whitelist()
@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
if is_single(df.doctype): continue
# optimized to get both link exists and parenttype
possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype
from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True)
possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype},
fields=['parenttype'], distinct=True)
if not possible_link: continue
@ -203,4 +308,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
"doctype_fieldname": df.doctype_fieldname
}
return ret
return ret

View file

@ -187,7 +187,7 @@ class Leaderboard {
render_search_box() {
this.$search_box =
$(`<div class="leaderboard-search col-md-3">
$(`<div class="leaderboard-search form-group col-md-3">
<input type="text" placeholder="Search" class="form-control leaderboard-search-input input-sm">
</div>`);
@ -363,7 +363,7 @@ class Leaderboard {
const link = `#Form/${this.options.selected_doctype}/${item.name}`;
const name_html = item.formatted_name ?
`<span class="text-muted ellipsis">${item.formatted_name}</span>`
`<span class="text-muted ellipsis list-id">${item.formatted_name}</span>`
: `<a class="grey list-id ellipsis" href="${link}"> ${item.name} </a>`;
const html =
`<div class="list-item">

View file

@ -229,8 +229,9 @@ def get_prepared_report_result(report, filters, dn="", user=None):
"status": "Completed",
"filters": json.dumps(filters),
"owner": user,
"report_name": report.report_name
}
"report_name": report.custom_report or report.report_name
},
order_by = 'creation desc'
)
if doc_list:
@ -424,9 +425,10 @@ def get_data_for_custom_report(columns):
def save_report(reference_report, report_name, columns):
report_doc = get_report_doc(reference_report)
docname = frappe.db.exists("Report", report_name)
docname = frappe.db.exists("Report",
{'report_name': report_name, 'is_standard': 'No', 'report_type': 'Custom Report'})
if docname:
report = frappe.get_doc("Report", {'report_name': docname, 'is_standard': 'No', 'report_type': 'Custom Report'})
report = frappe.get_doc("Report", docname)
report.update({"json": columns})
report.save()
frappe.msgprint(_("Report updated successfully"))

View file

@ -263,32 +263,8 @@ def delete_bulk(doctype, items):
@frappe.whitelist()
@frappe.read_only()
def get_sidebar_stats(stats, doctype, filters=[]):
_user_tags, tag_list = [], []
data = frappe._dict(frappe.local.form_dict)
filters = json.loads(data["filters"])
# Show Tags irrespective of any tag filter set
for idx, filter in enumerate(filters):
if filter[0] == "Tag Link":
filters.pop(idx)
break
for tag in frappe.get_all("Tag Link", filters={"document_type": doctype}, fields=["tag"]):
if tag.tag in tag_list:
continue
tag_list.append(tag.tag)
tag_filters = []
tag_filters.extend(filters)
tag_filters.extend([['Tag Link', 'tag', '=', tag.tag]])
fields = ["count(distinct `tab{0}`.`name`) AS total_count".format(doctype)]
count = frappe.get_all(doctype, filters=tag_filters, fields=fields)
if count[0].get("total_count") > 0:
_user_tags.append([tag.tag, count[0].get("total_count")])
return {"stats": {"_user_tags": _user_tags}}
return {"stats": get_stats(stats, doctype, filters)}
@frappe.whitelist()
@frappe.read_only()

View file

@ -247,13 +247,13 @@ class EmailAccount(Document):
exceptions = []
seen_status = []
uid_reindexed = False
email_server = None
if frappe.local.flags.in_test:
incoming_mails = test_mails
else:
email_sync_rule = self.build_email_sync_rule()
email_server = None
try:
email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
except Exception:
@ -294,7 +294,11 @@ class EmailAccount(Document):
else:
frappe.db.commit()
if communication:
attachments = [d.file_name for d in communication._attachments]
attachments = []
if hasattr(communication, '_attachments'):
attachments = [d.file_name for d in communication._attachments]
communication.notify(attachments=attachments, fetched_from_email_account=True)
#notify if user is linked to account
@ -305,7 +309,7 @@ class EmailAccount(Document):
raise Exception(frappe.as_json(exceptions))
def handle_bad_emails(self, email_server, uid, raw, reason):
if cint(email_server.settings.use_imap):
if email_server and cint(email_server.settings.use_imap):
import email
try:
mail = email.message_from_string(raw)

View file

@ -6,7 +6,7 @@ import frappe
import sys
from six.moves import html_parser as HTMLParser
import smtplib, quopri, json
from frappe import msgprint, _, safe_decode
from frappe import msgprint, _, safe_decode, safe_encode
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
from frappe.utils.verified_command import get_signed_params, verify_request
@ -563,7 +563,7 @@ def prepare_message(email, recipient, recipients_list):
print_format_file.update({"parent": message})
add_attachment(**print_format_file)
return message.as_string()
return safe_encode(message.as_string())
def clear_outbox():
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.

View file

@ -3,9 +3,10 @@
from __future__ import unicode_literals
import unittest, os, base64
from frappe import safe_decode
from frappe.email.receive import Email
from frappe.email.email_body import (replace_filename_with_cid,
get_email, inline_style_in_html, get_header)
get_email, inline_style_in_html, get_header)
from frappe.email.queue import prepare_message, get_email_queue
from six import PY3
@ -57,7 +58,7 @@ This is the text version of this email
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
text_content='whatever')
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
self.assertTrue("<h1>=EA=80=80abcd=DE=B4</h1>" in result)
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)
def test_prepare_message_returns_cr_lf(self):
email = get_email_queue(
@ -67,7 +68,8 @@ This is the text version of this email
content='<h1>\n this is a test of newlines\n' + '</h1>',
formatted='<h1>\n this is a test of newlines\n' + '</h1>',
text_content='whatever')
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
result = safe_decode(prepare_message(email=email,
recipient='test@test.com', recipients_list=[]))
if PY3:
self.assertTrue(result.count('\n') == result.count("\r"))
else:
@ -81,9 +83,10 @@ This is the text version of this email
subject='Test Subject',
content='<h1>Whatever</h1>',
text_content='whatever',
message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
".really.long.message.id.that.should.not.wrap.unti")
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
".really.long.message.id.that.should.not.wrap.unti")
result = safe_decode(prepare_message(email=email, recipient='test@test.com',
recipients_list=[]))
self.assertTrue(
"a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
".really.long.message.id.that.should.not.wrap.unti" in result)

View file

@ -1,10 +1,13 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2017-09-08 16:16:13.060641",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sb_doc_events",
"naming_series",
"webhook_doctype",
"cb_doc_events",
"webhook_docevent",
@ -43,6 +46,7 @@
{
"fieldname": "webhook_docevent",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Doc Event",
"options": "after_insert\non_update\non_submit\non_cancel\non_trash\non_update_after_submit\non_change",
"set_only_once": 1
@ -117,9 +121,16 @@
"fieldname": "webhook_json",
"fieldtype": "Code",
"label": "JSON Request Body"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "\nHOOK-.####"
}
],
"modified": "2019-08-26 00:38:14.611267",
"links": [],
"modified": "2020-01-06 02:51:07.997566",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
@ -140,5 +151,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "webhook_doctype",
"track_changes": 1
}

View file

@ -18,9 +18,6 @@ from frappe.utils.jinja import validate_template
class Webhook(Document):
def autoname(self):
self.name = self.webhook_doctype + "-" + self.webhook_docevent
def validate(self):
self.validate_docevent()
self.validate_condition()

View file

@ -31,6 +31,10 @@ def login_via_office365(code, state):
def login_via_salesforce(code, state):
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def login_via_fairlogin(code, state):
login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def custom(code, state):
"""

View file

@ -648,18 +648,96 @@ frappe.ui.form.Form = class FrappeForm {
}
savecancel(btn, callback, on_error) {
var me = this;
const me = this;
this.validate_form_action('Cancel');
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() {
frappe.call({
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
args: {
doctype: me.doc.doctype,
name: me.doc.name
},
freeze: true,
callback: (r) => {
if (!r.exc && r.message.count > 0) {
me._cancel_all(r, btn, callback, on_error);
} else {
me._cancel(btn, callback, on_error, false);
}
}
});
}
_cancel_all(r, btn, callback, on_error) {
const me = this;
// add confirmation message for cancelling all linked docs
let links_text = "";
let links = r.message.docs;
const doctypes = Array.from(new Set(links.map(link => link.doctype)));
for (let doctype of doctypes) {
let docnames = links
.filter((link) => link.doctype == doctype)
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
.join(", ");
links_text += `<li><strong>${doctype}</strong>: ${docnames}</li>`;
}
links_text = `<ul>${links_text}</ul>`;
let confirm_message = __('{0} {1} is linked with the following submitted documents: {2}',
[(me.doc.doctype).bold(), me.doc.name, links_text]);
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
if (can_cancel) {
confirm_message += __('Do you want to cancel all linked documents?');
} else {
confirm_message += __('You do not have permissions to cancel all linked documents.');
}
// generate dialog box to cancel all linked docs
let d = new frappe.ui.Dialog({
title: __("Cancel All Documents"),
fields: [{
fieldtype: "HTML",
options: `<p class="frappe-confirm-message">${confirm_message}</p>`
}]
}, () => me.handle_save_fail(btn, on_error));
// if user can cancel all linked docs, add action to the dialog
if (can_cancel) {
d.set_primary_action("Cancel All", () => {
d.hide();
frappe.call({
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
args: {
docs: links
},
freeze: true,
callback: (resp) => {
if (!resp.exc) {
me.reload_doc();
me._cancel(btn, callback, on_error, true);
}
}
});
});
}
d.show();
};
_cancel(btn, callback, on_error, skip_confirm) {
const me = this;
const cancel_doc = () => {
frappe.validated = true;
me.script_manager.trigger("before_cancel").then(function() {
if(!frappe.validated) {
me.script_manager.trigger("before_cancel").then(() => {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
}
var after_cancel = function(r) {
if(r.exc) {
if (r.exc) {
me.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("cancel");
@ -670,8 +748,14 @@ frappe.ui.form.Form = class FrappeForm {
};
frappe.ui.form.save(me, "cancel", after_cancel, btn);
});
}, () => me.handle_save_fail(btn, on_error));
}
}
if (skip_confirm) {
cancel_doc();
} else {
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
}
};
savetrash() {
this.validate_form_action("Delete");

View file

@ -219,7 +219,7 @@ export default class Grid {
this.remove_rows_button.toggleClass('hidden',
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
this.remove_all_rows_button.toggleClass('hidden',
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true);
}
get_selected() {
@ -426,7 +426,7 @@ export default class Grid {
}
},
onUpdate: (event) => {
let idx = $(event.item).closest('.grid-row').attr('data-idx');
let idx = $(event.item).closest('.grid-row').attr('data-idx') - 1;
let doc = this.data[idx%this.grid_pagination.page_length];
this.renumber_based_on_dom();
this.frm.script_manager.trigger(this.df.fieldname + "_move", this.df.options, doc.name);

View file

@ -69,16 +69,65 @@ frappe.ui.form.Toolbar = Class.extend({
can_rename: function() {
return this.frm.perm[0].write && this.frm.meta.allow_rename && !this.frm.doc.__islocal;
},
show_unchanged_document_alert: function() {
frappe.show_alert({
indicator: "yellow",
message: __("Unchanged")
});
},
rename_document_title(new_name, new_title, merge=false) {
const docname = this.frm.doc.name;
const title_field = this.frm.meta.title_field || '';
const doctype = this.frm.doctype;
let confirm_message=null;
if (new_name) {
const warning = __("This cannot be undone");
const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]);
confirm_message = `${message}<br><b>${warning}<b>`;
}
let rename_document = () => {
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,
merge
}).then(new_docname => {
if (new_name != docname) {
$(document).trigger("rename", [doctype, docname, new_docname || new_name]);
if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname];
}
this.frm.reload_doc();
});
};
return new Promise((resolve, reject) => {
if (new_title === this.frm.doc[title_field] && new_name === docname) {
this.show_unchanged_document_alert();
resolve();
} else if (merge) {
frappe.confirm(confirm_message, () => {
rename_document().then(resolve).catch(reject);
}, reject);
} else {
rename_document().then(resolve).catch(reject);
}
});
},
setup_editable_title: function () {
let me = this;
this.page.$title_area.find(".title-text").on("click", () => {
let fields = [];
let doctype = me.frm.doctype;
let docname = me.frm.doc.name;
let title_field = me.frm.meta.title_field || '';
// check if title is updateable
// check if title is updatable
if (me.is_title_editable()) {
let title_field_label = me.frm.get_docfield(title_field).label;
@ -91,7 +140,7 @@ frappe.ui.form.Toolbar = Class.extend({
});
}
// check if docname is updateable
// check if docname is updatable
if (me.can_rename()) {
fields.push(...[{
label: __("New Name"),
@ -114,36 +163,15 @@ frappe.ui.form.Toolbar = Class.extend({
fields: fields
});
d.show();
d.set_primary_action(__("Rename"), function () {
let args = d.get_values();
if (args.title != me.frm.doc[title_field] || args.name != docname) {
frappe.call({
method: "frappe.model.rename_doc.update_document_title",
args: {
doctype,
docname,
title_field,
old_title: me.frm.doc[title_field],
new_title: args.title,
new_name: args.name,
merge: args.merge
},
btn: d.get_primary_btn()
}).then((res) => {
me.frm.reload_doc();
if (!res.exc && (args.name != docname)) {
$(document).trigger("rename", [doctype, docname, res.message || args.name]);
if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname];
}
d.set_primary_action(__("Rename"), (values) => {
d.disable_primary_action();
this.rename_document_title(values.name, values.title, values.merge)
.then(() => {
d.hide();
})
.catch(() => {
d.enable_primary_action();
});
} else {
frappe.show_alert({
indicator: "yellow",
message: __("Unchanged")
});
}
d.hide();
});
}
});

View file

@ -335,14 +335,13 @@ frappe.views.ListSidebar = class ListSidebar {
field: field,
stat: stats,
sum: sum,
label: field === '_user_tags' ? (tags ? __(label) : __("Tag")) : __(label),
label: field === '_user_tags' ? (tags ? __(label) : __("Tags")) : __(label),
};
$(frappe.render_template("list_sidebar_stat", context))
.on("click", ".stat-link", function() {
var doctype = "Tag Link";
var fieldname = $(this).attr('data-field');
var label = $(this).attr('data-label');
var condition = "=";
var condition = "like";
var existing = me.list_view.filter_area.filter_list.get_filter(fieldname);
if(existing) {
existing.remove();
@ -351,7 +350,7 @@ frappe.views.ListSidebar = class ListSidebar {
label = "%,%";
condition = "not like";
}
me.list_view.filter_area.filter_list.add_filter(doctype, fieldname, condition, label)
me.list_view.filter_area.filter_list.add_filter(me.list_view.doctype, fieldname, condition, label)
.then(function() {
me.list_view.refresh();
});

View file

@ -492,7 +492,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
tag_editor.wrapper.on('click', '.tagit-label', (e) => {
const $this = $(e.currentTarget);
this.filter_area.add('Tag Link', 'tag', '=', $this.text());
this.filter_area.add(this.doctype, '_user_tags', '=', $this.text());
});
});
}

View file

@ -550,23 +550,28 @@ $.extend(frappe.model, {
},
rename_doc: function(doctype, docname, callback) {
let message = __("Merge with existing");
let warning = __("This cannot be undone");
let merge_label = message + " <b>(" + warning + ")</b>";
var d = new frappe.ui.Dialog({
title: __("Rename {0}", [__(docname)]),
fields: [
{label:__("New Name"), fieldname: "new_name", fieldtype:"Data", reqd:1, "default": docname},
{label:__("Merge with existing"), fieldtype:"Check", fieldname:"merge"},
{label: __("New Name"), fieldname: "new_name", fieldtype: "Data", reqd: 1, "default": docname},
{label: merge_label, fieldtype: "Check", fieldname: "merge"},
]
});
d.set_primary_action(__("Rename"), function() {
var args = d.get_values();
if(!args) return;
return frappe.call({
method:"frappe.model.rename_doc.rename_doc",
method:"frappe.rename_doc",
args: {
doctype: doctype,
old: docname,
"new": args.new_name,
"merge": args.merge
new: args.new_name,
merge: args.merge
},
btn: d.get_primary_btn(),
callback: function(r,rt) {

View file

@ -174,11 +174,6 @@ frappe.ui.Filter = class {
let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname];
if (doctype === "Tag Link" || fieldname === "_user_tags") {
original_docfield = {fieldname: "tag", fieldtype: "Data", label: "Tags", parent: "Tag Link"};
doctype = "Tag Link";
}
if(!original_docfield) {
console.warn(`Field ${fieldname} is not selectable.`);
this.remove();

View file

@ -63,11 +63,6 @@ frappe.ui.FilterGroup = class {
}
validate_args(doctype, fieldname) {
// Tags attached to the document are maintained seperately in Tag Link
// and is not the part of doctype meta therefore tag fieldname validation is ignored.
if (doctype === "Tag Link" && fieldname === "tag") {
return true;
}
if(doctype && fieldname
&& !frappe.meta.has_field(doctype, fieldname)

View file

@ -108,6 +108,11 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
});
}
get_fields() {
this.fields.push([this.board.field_name, this.board.reference_doctype]);
return super.get_fields();
}
render() {
const board_name = this.board_name;
if (this.kanban && board_name === this.kanban.board_name) {

View file

@ -29,13 +29,13 @@
<!-- body -->
<tbody>
{% for row in data %}
<tr>
<tr style="height: 30px">
{% for col in columns %}
{% if col.name && col._id !== "_check" %}
{% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %}
<td>
<td {% if row.bold == 1 %} style="font-weight: bold" {% endif %}>
<span {% if col._index == 0 %} style="padding-left: {%= cint(row.indent) * 2 %}em" {% endif %}>
{{
col.formatter

View file

@ -186,7 +186,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
let report_script_name = this.report_doc.report_type === 'Custom Report'
? this.report_doc.reference_report
: this.report_name;
return frappe.query_reports[report_script_name];
return frappe.query_reports[report_script_name] || {};
}
setup_progress_bar() {

View file

@ -86,7 +86,11 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
setup_delete_button() {
this.add_button_to_header("Delete", "danger", () => this.delete());
this.add_button_to_header(
'<i class="fa fa-trash" aria-hidden="true"></i>',
"light",
() => this.delete()
);
}
setup_print_button() {

View file

@ -201,7 +201,7 @@ export default class WebFormList {
() => (window.location.href = window.location.pathname + "?new=1")
);
if (this.rows.length <= this.page_length) {
if (this.rows.length > this.page_length) {
addButton(footer, "more", "secondary", false, "More", () => this.more());
}

View file

@ -66,7 +66,7 @@ login.bind_events = function() {
}
});
{% if ldap_settings.enabled %}
{% if ldap_settings and ldap_settings.enabled %}
$(".btn-ldap-login").on("click", function(){
var args = {};
args.cmd = "{{ ldap_settings.method }}";

View file

@ -1,7 +1,7 @@
<nav class="navbar navbar-light bg-white navbar-expand-lg sticky-top shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url_prefix }}{{ home_page or "/" }}">
<span>{{ brand_html or (frappe.get_hooks("brand_html") or ["Home"])[0] }}</span>
<span>{{ brand_html or (frappe.get_hooks("brand_html") or [_("Home")])[0] }}</span>
</a>
<button class="navbar-toggler" type="button"
data-toggle="collapse"
@ -17,4 +17,4 @@
{% include "templates/includes/navbar/navbar_items.html" %}
</div>
</div>
</nav>
</nav>

View file

@ -27,6 +27,9 @@
</button>
</div>
</div>
{% if frappe.form_dict.scope %}
<input type="text" hidden name="scope" value="{{ frappe.form_dict.scope }}">
{% endif %}
</form>
</div>
</div>

View file

@ -41,7 +41,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
xmloutput_fh = None
if junit_xml_output:
xmloutput_fh = open(junit_xml_output, 'w')
xmloutput_fh = open(junit_xml_output, 'wb')
unittest_runner = xmlrunner_wrapper(xmloutput_fh)
else:
unittest_runner = unittest.TextTestRunner
@ -68,11 +68,11 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
frappe.get_attr(fn)()
if doctype:
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile)
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile, junit_xml_output=junit_xml_output)
elif module:
ret = run_tests_for_module(module, verbose, tests, profile)
ret = run_tests_for_module(module, verbose, tests, profile, junit_xml_output=junit_xml_output)
else:
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast)
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output)
if frappe.db: frappe.db.commit()
@ -109,7 +109,7 @@ class TimeLoggingTestResult(unittest.TextTestResult):
super(TimeLoggingTestResult, self).addSuccess(test)
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False):
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False, junit_xml_output=False):
import os
apps = [app] if app else frappe.get_installed_apps()
@ -130,11 +130,16 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
_add_test(app, path, filename, verbose,
test_suite, ui_tests)
if junit_xml_output:
runner = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast)
else:
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast)
if profile:
pr = cProfile.Profile()
pr.enable()
out = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite)
out = runner.run(test_suite)
if profile:
pr.disable()
@ -145,7 +150,7 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
return out
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False):
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False, junit_xml_output=False):
modules = []
if not isinstance(doctypes, (list, tuple)):
doctypes = [doctypes]
@ -163,17 +168,17 @@ def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profil
make_test_records(doctype, verbose=verbose, force=force)
modules.append(importlib.import_module(test_module))
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile)
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def run_tests_for_module(module, verbose=False, tests=(), profile=False):
def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_xml_output=False):
module = importlib.import_module(module)
if hasattr(module, "test_dependencies"):
for doctype in module.test_dependencies:
make_test_records(doctype, verbose=verbose)
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile)
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def _run_unittest(modules, verbose=False, tests=(), profile=False):
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
test_suite = unittest.TestSuite()
if not isinstance(modules, (list, tuple)):
@ -189,13 +194,18 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
else:
test_suite.addTest(module_test_cases)
if junit_xml_output:
runner = unittest_runner(verbosity=1+(verbose and 1 or 0))
else:
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0))
if profile:
pr = cProfile.Profile()
pr.enable()
frappe.flags.tests_verbose = verbose
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
out = runner.run(test_suite)
if profile:

View file

@ -2,9 +2,14 @@
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, unittest, os
from frappe.utils import cint
import os
import unittest
import frappe
from frappe.utils import cint, add_to_date, now
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
from frappe.exceptions import DoesNotExistError
class TestDocument(unittest.TestCase):
def test_get_return_empty_list_for_table_field_if_none(self):
@ -236,3 +241,44 @@ class TestDocument(unittest.TestCase):
new_current = cint(frappe.db.get_value('Series', prefix, "current", order_by="name"))
self.assertEqual(cint(old_current) - 1, new_current)
def test_rename_doc(self):
from random import choice, sample
available_documents = []
doctype = "ToDo"
# data generation: 4 todo documents
for num in range(1, 5):
doc = frappe.get_doc({
"doctype": doctype,
"date": add_to_date(now(), days=num),
"description": "this is todo #{}".format(num)
}).insert()
available_documents.append(doc.name)
# test 1: document renaming
old_name = choice(available_documents)
new_name = old_name + '.new'
self.assertEqual(new_name, frappe.rename_doc(doctype, old_name, new_name, force=True))
available_documents.remove(old_name)
available_documents.append(new_name)
# test 2: merge documents
first_todo, second_todo = sample(available_documents, 2)
second_todo_doc = frappe.get_doc(doctype, second_todo)
second_todo_doc.priority = "High"
second_todo_doc.save()
merged_todo = frappe.rename_doc(doctype, first_todo, second_todo, merge=True, force=True)
merged_todo_doc = frappe.get_doc(doctype, merged_todo)
available_documents.remove(first_todo)
with self.assertRaises(DoesNotExistError):
frappe.get_doc(doctype, first_todo)
self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority)
for docname in available_documents:
frappe.delete_doc(doctype, docname)

View file

@ -142,6 +142,8 @@ def start_worker(queue=None, quiet = False):
with Connection(redis_connection):
queues = get_queue_list(queue)
logging_level = "INFO"
if quiet:
logging_level = "WARNING"
Worker(queues, name=get_worker_name(queue)).work(logging_level = logging_level)
def get_worker_name(queue):

View file

@ -256,6 +256,10 @@ app_license = "{app_license}"
# "Task": "{app_name}.task.get_dashboard_data"
# }}
# exempt linked doctypes from being automatically cancelled
#
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
"""
desktop_template = """# -*- coding: utf-8 -*-

View file

@ -81,6 +81,10 @@ def rebuild_for_doctype(doctype):
return filters
meta = frappe.get_meta(doctype)
if cint(meta.issingle) == 1:
return
if cint(meta.istable) == 1:
parent_doctypes = frappe.get_all("DocField", fields="parent", filters={
"fieldtype": ["in", frappe.model.table_fields],
@ -137,8 +141,8 @@ def rebuild_for_doctype(doctype):
"name": frappe.db.escape(doc.name),
"content": frappe.db.escape(' ||| '.join(content or '')),
"published": published,
"title": frappe.db.escape(title or '')[:int(frappe.db.VARCHAR_LEN)],
"route": frappe.db.escape(route or '')[:int(frappe.db.VARCHAR_LEN)]
"title": frappe.db.escape((title or '')[:int(frappe.db.VARCHAR_LEN)]),
"route": frappe.db.escape((route or '')[:int(frappe.db.VARCHAR_LEN)])
})
if all_contents:
insert_values_for_multiple_docs(all_contents)

View file

@ -88,6 +88,20 @@ class TestWorkflow(unittest.TestCase):
self.assertEqual(workflow_actions[0].status, 'Completed')
frappe.set_user('Administrator')
def test_update_docstatus(self):
todo = create_new_todo()
apply_workflow(todo, 'Approve')
self.workflow.states[1].doc_status = 0
self.workflow.save()
todo.reload()
self.assertEqual(todo.docstatus, 0)
self.workflow.states[1].doc_status = 1
self.workflow.save()
todo.reload()
self.assertEqual(todo.docstatus, 1)
def create_todo_workflow():
if frappe.db.exists('Workflow', 'Test ToDo'):
return frappe.get_doc('Workflow', 'Test ToDo').save(ignore_permissions=True)

View file

@ -16,6 +16,7 @@ class Workflow(Document):
self.validate_docstatus()
def on_update(self):
self.update_doc_status()
frappe.clear_cache(doctype=self.document_type)
frappe.cache().delete_key('workflow_' + self.name) # clear cache created in model/workflow.py
@ -56,6 +57,29 @@ class Workflow(Document):
docstatus_map[d.doc_status] = d.state
def update_doc_status(self):
'''
Checks if the docstatus of a state was updated.
If yes then the docstatus of the document with same state will be updated
'''
doc_before_save = self.get_doc_before_save()
before_save_states, new_states = {}, {}
if doc_before_save:
for d in doc_before_save.states:
before_save_states[d.state] = d
for d in self.states:
new_states[d.state] = d
for key in new_states:
if key in before_save_states:
if not new_states[key].doc_status == before_save_states[key].doc_status:
frappe.db.set_value(self.document_type, {
self.workflow_state_field: before_save_states[key].state
},
'docstatus',
new_states[key].doc_status,
update_modified = False)
def validate_docstatus(self):
def get_state(state):
for s in self.states: