diff --git a/frappe/__init__.py b/frappe/__init__.py
index b383ae958e..4ab86e6b5d 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -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 = '
' + ''.join(['
'+''.join(['
%s
' % c for c in r])+'
' for r in msg]) + '
'
- 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 += '
{}
'.format(data)
+ table_rows += '
{}
'.format(table_row_data)
+
+ out.message = '''
{}
'''.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 '{2} {1}'.format(doctype, name, _(doctype))
+ html = '{doctype_local} {name}'
+ return html.format(
+ doctype=doctype,
+ name=name,
+ doctype_local=_(doctype)
+ )
def bold(text):
return '{0}'.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": # 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": # 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)
diff --git a/frappe/build.py b/frappe/build.py
index f7437acf8f..761541f7a9 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -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))
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 11f8e69b95..c48cd2f7b1 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -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
]
diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py
index 4e8ffb00d5..538aa30390 100644
--- a/frappe/config/desktop.py
+++ b/frappe/config/desktop.py
@@ -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,
},
diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py
index b3392bf4ad..22c4778147 100644
--- a/frappe/core/doctype/data_import/importer_new.py
+++ b/frappe/core/doctype/data_import/importer_new.py
@@ -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
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 8d8731e012..969a71ab7d 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -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()
\ No newline at end of file
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index ac89b157fa..6633884bb3 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -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
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 0e2afbb35c..63aa93e7ff 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -171,7 +171,6 @@ def get_documents_for_tag(tag):
"content": res.title
})
- print(results)
return results
@frappe.whitelist()
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 734b99a003..6c679bf312 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -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
\ No newline at end of file
+ return ret
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index c64d2dcb4f..5359e9eab8 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -187,7 +187,7 @@ class Leaderboard {
render_search_box() {
this.$search_box =
- $(`
+ $(`
`);
@@ -363,7 +363,7 @@ class Leaderboard {
const link = `#Form/${this.options.selected_doctype}/${item.name}`;
const name_html = item.formatted_name ?
- `${item.formatted_name}`
+ `${item.formatted_name}`
: ` ${item.name} `;
const html =
`
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 7dc561193f..dca1a99802 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -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"))
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index c0685b67f2..db37bc6df0 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -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()
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 50daf1cf72..a108225a48 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -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)
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index 4a0a34c76e..62b0d9ea3f 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -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.
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 26c4e5ba5d..f44c6e775a 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -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='
' + uni_chr1 + 'abcd' + uni_chr2 + '
',
text_content='whatever')
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
- self.assertTrue("
=EA=80=80abcd=DE=B4
" in result)
+ self.assertTrue(b"
=EA=80=80abcd=DE=B4
" 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='
\n this is a test of newlines\n' + '
',
formatted='
\n this is a test of newlines\n' + '
',
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='
`;
+
+ 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: `