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(['' % c for c in r])+'' for r in msg]) + '
%s
' - 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 = - $(` + {% if frappe.form_dict.scope %} + + {% endif %} diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 0be19c6110..b66a96595d 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -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: diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 4c48ef2811..1e92015602 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -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) \ No newline at end of file diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index c1ac7581dc..65b2326733 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -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): diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 4a7b93751a..b81d802a07 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -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 -*- diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 7012b737c0..4b50745a74 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -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) diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index 52a94d59c6..66c0577026 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -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) diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index c3f0991d85..62e0b39b08 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -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: