Merge branch 'develop' of https://github.com/frappe/frappe into force_listview_columns
This commit is contained in:
commit
b779767d2d
41 changed files with 743 additions and 339 deletions
|
|
@ -6,7 +6,7 @@ globals attached to frappe module
|
|||
"""
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from six import iteritems, binary_type, text_type, string_types
|
||||
from six import iteritems, binary_type, text_type, string_types, PY2
|
||||
from werkzeug.local import Local, release_local
|
||||
import os, sys, importlib, inspect, json
|
||||
from past.builtins import cmp
|
||||
|
|
@ -19,7 +19,7 @@ from .utils.jinja import (get_jenv, get_template, render_template, get_email_fro
|
|||
|
||||
# Harmless for Python 3
|
||||
# For Python 2 set default encoding to utf-8
|
||||
if sys.version[0] == '2':
|
||||
if PY2:
|
||||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
|
|
@ -321,10 +321,19 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
|
|||
return
|
||||
|
||||
if as_table and type(msg) in (list, tuple):
|
||||
out.msg = '<table border="1px" style="border-collapse: collapse" cellpadding="2px">' + ''.join(['<tr>'+''.join(['<td>%s</td>' % c for c in r])+'</tr>' for r in msg]) + '</table>'
|
||||
|
||||
if flags.print_messages and out.msg:
|
||||
print("Message: " + repr(out.msg).encode("utf-8"))
|
||||
table_rows = ''
|
||||
for row in msg:
|
||||
table_row_data = ''
|
||||
for data in row:
|
||||
table_row_data += '<td>{}</td>'.format(data)
|
||||
table_rows += '<tr>{}</tr>'.format(table_row_data)
|
||||
|
||||
out.message = '''<table class="table table-bordered"
|
||||
style="margin: 0;">{}</table>'''.format(table_rows)
|
||||
|
||||
if flags.print_messages and out.message:
|
||||
print("Message: " + repr(out.message).encode("utf-8"))
|
||||
|
||||
if title:
|
||||
out.title = title
|
||||
|
|
@ -363,7 +372,6 @@ def throw(msg, exc=ValidationError, title=None):
|
|||
msgprint(msg, raise_exception=exc, title=title, indicator='red')
|
||||
|
||||
def emit_js(js, user=False, **kwargs):
|
||||
from frappe.realtime import publish_realtime
|
||||
if user == False:
|
||||
user = session.user
|
||||
publish_realtime('eval_js', js, user=user, **kwargs)
|
||||
|
|
@ -767,8 +775,8 @@ def get_meta_module(doctype):
|
|||
import frappe.modules
|
||||
return frappe.modules.load_doctype_module(doctype)
|
||||
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
|
||||
ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None,
|
||||
for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
|
||||
"""Delete a document. Calls `frappe.model.delete_doc.delete_doc`.
|
||||
|
||||
:param doctype: DocType of document to be delete.
|
||||
|
|
@ -803,8 +811,14 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False):
|
|||
import frappe.modules
|
||||
return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions)
|
||||
|
||||
@whitelist()
|
||||
def rename_doc(*args, **kwargs):
|
||||
"""Rename a document. Calls `frappe.model.rename_doc.rename_doc`"""
|
||||
"""
|
||||
Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link"
|
||||
|
||||
Calls `frappe.model.rename_doc.rename_doc`
|
||||
"""
|
||||
kwargs.pop('ignore_permissions', None)
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
return rename_doc(*args, **kwargs)
|
||||
|
||||
|
|
@ -814,11 +828,11 @@ def get_module(modulename):
|
|||
|
||||
def scrub(txt):
|
||||
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
|
||||
return txt.replace(' ','_').replace('-', '_').lower()
|
||||
return txt.replace(' ', '_').replace('-', '_').lower()
|
||||
|
||||
def unscrub(txt):
|
||||
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
|
||||
return txt.replace('_',' ').replace('-', ' ').title()
|
||||
return txt.replace('_', ' ').replace('-', ' ').title()
|
||||
|
||||
def get_module_path(module, *joins):
|
||||
"""Get the path of the given module name.
|
||||
|
|
@ -980,7 +994,8 @@ def setup_module_map():
|
|||
if not (local.app_modules and local.module_app):
|
||||
local.module_app, local.app_modules = {}, {}
|
||||
for app in get_all_apps(True):
|
||||
if app=="webnotes": app="frappe"
|
||||
if app == "webnotes":
|
||||
app = "frappe"
|
||||
local.app_modules.setdefault(app, [])
|
||||
for module in get_module_list(app):
|
||||
module = scrub(module)
|
||||
|
|
@ -999,7 +1014,10 @@ def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
|
|||
if content:
|
||||
content = frappe.utils.strip(content)
|
||||
|
||||
return [p.strip() for p in content.splitlines() if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))]
|
||||
return [
|
||||
p.strip() for p in content.splitlines()
|
||||
if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
|
@ -1161,8 +1179,8 @@ def compare(val1, condition, val2):
|
|||
import frappe.utils
|
||||
return frappe.utils.compare(val1, condition, val2)
|
||||
|
||||
def respond_as_web_page(title, html, success=None, http_status_code=None,
|
||||
context=None, indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
|
||||
def respond_as_web_page(title, html, success=None, http_status_code=None, context=None,
|
||||
indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
|
||||
width=None, template='message'):
|
||||
"""Send response as a web page with a message rather than JSON. Used to show permission errors etc.
|
||||
|
||||
|
|
@ -1351,7 +1369,8 @@ def format(*args, **kwargs):
|
|||
import frappe.utils.formatters
|
||||
return frappe.utils.formatters.format_value(*args, **kwargs)
|
||||
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None, output = None, no_letterhead = 0, password=None):
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None,
|
||||
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
|
||||
"""Get Print Format for given document.
|
||||
|
||||
:param doctype: DocType of document.
|
||||
|
|
@ -1382,7 +1401,8 @@ def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
|
|||
else:
|
||||
return html
|
||||
|
||||
def attach_print(doctype, name, file_name=None, print_format=None, style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
|
||||
def attach_print(doctype, name, file_name=None, print_format=None,
|
||||
style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
|
||||
from frappe.utils import scrub_urls
|
||||
|
||||
if not file_name: file_name = name
|
||||
|
|
@ -1398,16 +1418,28 @@ def attach_print(doctype, name, file_name=None, print_format=None, style=None, h
|
|||
|
||||
no_letterhead = not print_letterhead
|
||||
|
||||
kwargs = dict(
|
||||
print_format=print_format,
|
||||
style=style,
|
||||
html=html,
|
||||
doc=doc,
|
||||
no_letterhead=no_letterhead,
|
||||
password=password
|
||||
)
|
||||
|
||||
content = ''
|
||||
if int(print_settings.send_print_as_pdf or 0):
|
||||
out = {
|
||||
"fname": file_name + ".pdf",
|
||||
"fcontent": get_print(doctype, name, print_format=print_format, style=style, html=html, as_pdf=True, doc=doc, no_letterhead=no_letterhead, password=password)
|
||||
}
|
||||
ext = ".pdf"
|
||||
kwargs["as_pdf"] = True
|
||||
content = get_print(doctype, name, **kwargs)
|
||||
else:
|
||||
out = {
|
||||
"fname": file_name + ".html",
|
||||
"fcontent": scrub_urls(get_print(doctype, name, print_format=print_format, style=style, html=html, doc=doc, no_letterhead=no_letterhead, password=password)).encode("utf-8")
|
||||
}
|
||||
ext = ".html"
|
||||
content = scrub_urls(get_print(doctype, name, **kwargs)).encode('utf-8')
|
||||
|
||||
out = {
|
||||
"fname": file_name + ext,
|
||||
"fcontent": content
|
||||
}
|
||||
|
||||
local.flags.ignore_print_permissions = False
|
||||
#reset lang to original local lang
|
||||
|
|
@ -1526,7 +1558,12 @@ def log_error(message=None, title=None):
|
|||
method=title)).insert(ignore_permissions=True)
|
||||
|
||||
def get_desk_link(doctype, name):
|
||||
return '<a href="#Form/{0}/{1}" style="font-weight: bold;">{2} {1}</a>'.format(doctype, name, _(doctype))
|
||||
html = '<a href="#Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
|
||||
return html.format(
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
doctype_local=_(doctype)
|
||||
)
|
||||
|
||||
def bold(text):
|
||||
return '<b>{0}</b>'.format(text)
|
||||
|
|
@ -1545,10 +1582,9 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
|
|||
|
||||
if not eval_globals:
|
||||
eval_globals = {}
|
||||
|
||||
eval_globals['__builtins__'] = {}
|
||||
|
||||
eval_globals.update(whitelisted_globals)
|
||||
|
||||
return eval(code, eval_globals, eval_locals)
|
||||
|
||||
def get_system_settings(key):
|
||||
|
|
@ -1560,49 +1596,46 @@ def get_active_domains():
|
|||
from frappe.core.doctype.domain_settings.domain_settings import get_active_domains
|
||||
return get_active_domains()
|
||||
|
||||
def get_version(doctype, name, limit = None, head = False, raise_err = True):
|
||||
def get_version(doctype, name, limit=None, head=False, raise_err=True):
|
||||
'''
|
||||
Returns a list of version information of a given DocType (Applicable only if DocType has changes tracked).
|
||||
Returns a list of version information of a given DocType.
|
||||
|
||||
Note: Applicable only if DocType has changes tracked.
|
||||
|
||||
Example
|
||||
>>> frappe.get_version('User', 'foobar@gmail.com')
|
||||
>>>
|
||||
[
|
||||
{
|
||||
"version": [version.data], # Refer Version DocType get_diff method and data attribute
|
||||
"user": "admin@gmail.com" # User that created this version
|
||||
"creation": <datetime.datetime> # Creation timestamp of that object.
|
||||
"version": [version.data], # Refer Version DocType get_diff method and data attribute
|
||||
"user": "admin@gmail.com", # User that created this version
|
||||
"creation": <datetime.datetime> # Creation timestamp of that object.
|
||||
}
|
||||
]
|
||||
'''
|
||||
meta = get_meta(doctype)
|
||||
meta = get_meta(doctype)
|
||||
if meta.track_changes:
|
||||
names = db.sql("""
|
||||
SELECT name from tabVersion
|
||||
WHERE ref_doctype = '{doctype}' AND docname = '{name}'
|
||||
{order_by}
|
||||
{limit}
|
||||
""".format(
|
||||
doctype = doctype,
|
||||
name = name,
|
||||
order_by = 'ORDER BY creation' if head else '',
|
||||
limit = 'LIMIT {limit}'.format(limit = limit) if limit else ''
|
||||
))
|
||||
names = db.get_all('Version', filters={
|
||||
'ref_doctype': doctype,
|
||||
'docname': name,
|
||||
'order_by': 'creation' if head else None,
|
||||
'limit': limit
|
||||
}, as_list=1)
|
||||
|
||||
from frappe.chat.util import squashify, dictify, safe_json_loads
|
||||
|
||||
versions = [ ]
|
||||
versions = []
|
||||
|
||||
for name in names:
|
||||
name = squashify(name)
|
||||
doc = get_doc('Version', name)
|
||||
doc = get_doc('Version', name)
|
||||
|
||||
data = doc.data
|
||||
data = safe_json_loads(data)
|
||||
data = dictify(dict(
|
||||
version = data,
|
||||
user = doc.owner,
|
||||
creation = doc.creation
|
||||
version=data,
|
||||
user=doc.owner,
|
||||
creation=doc.creation
|
||||
))
|
||||
|
||||
versions.append(data)
|
||||
|
|
@ -1610,16 +1643,14 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True):
|
|||
return versions
|
||||
else:
|
||||
if raise_err:
|
||||
raise ValueError('{doctype} has no versions tracked.'.format(
|
||||
doctype = doctype
|
||||
))
|
||||
raise ValueError(_('{0} has no versions tracked.').format(doctype))
|
||||
|
||||
@whitelist(allow_guest = True)
|
||||
@whitelist(allow_guest=True)
|
||||
def ping():
|
||||
return "pong"
|
||||
|
||||
|
||||
def safe_encode(param, encoding = 'utf-8'):
|
||||
def safe_encode(param, encoding='utf-8'):
|
||||
try:
|
||||
param = param.encode(encoding)
|
||||
except Exception:
|
||||
|
|
@ -1627,7 +1658,7 @@ def safe_encode(param, encoding = 'utf-8'):
|
|||
return param
|
||||
|
||||
|
||||
def safe_decode(param, encoding = 'utf-8'):
|
||||
def safe_decode(param, encoding='utf-8'):
|
||||
try:
|
||||
param = param.decode(encoding)
|
||||
except Exception:
|
||||
|
|
@ -1638,9 +1669,9 @@ def parse_json(val):
|
|||
from frappe.utils import parse_json
|
||||
return parse_json(val)
|
||||
|
||||
def mock(type, size = 1, locale = 'en'):
|
||||
results = [ ]
|
||||
faker = Faker(locale)
|
||||
def mock(type, size=1, locale='en'):
|
||||
results = []
|
||||
faker = Faker(locale)
|
||||
if not type in dir(faker):
|
||||
raise ValueError('Not a valid mock type.')
|
||||
else:
|
||||
|
|
@ -1649,7 +1680,4 @@ def mock(type, size = 1, locale = 'en'):
|
|||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
|
||||
results = squashify(results)
|
||||
|
||||
return results
|
||||
return squashify(results)
|
||||
|
|
|
|||
133
frappe/build.py
133
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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -171,7 +171,6 @@ def get_documents_for_tag(tag):
|
|||
"content": res.title
|
||||
})
|
||||
|
||||
print(results)
|
||||
return results
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,119 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, json
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from six import string_types
|
||||
import frappe
|
||||
import frappe.desk.form.load
|
||||
import frappe.desk.form.meta
|
||||
from frappe import _
|
||||
from frappe.model.meta import is_single
|
||||
from frappe.modules import load_doctype_module
|
||||
import frappe.desk.form.meta
|
||||
import frappe.desk.form.load
|
||||
from six import string_types
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None):
|
||||
"""
|
||||
Get all nested submitted linked doctype linkinfo
|
||||
|
||||
Arguments:
|
||||
doctype (str) - The doctype for which get all linked doctypes
|
||||
name (str) - The docname for which get all linked doctypes
|
||||
|
||||
Keyword Arguments:
|
||||
docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
|
||||
|
||||
Returns:
|
||||
dict - Return list of documents and link count
|
||||
"""
|
||||
|
||||
if not docs:
|
||||
docs = []
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
for link_doctype, link_names in linked_docs.items():
|
||||
for link in link_names:
|
||||
docinfo = link.update({"doctype": link_doctype})
|
||||
validated_doc = validate_linked_doc(docinfo)
|
||||
|
||||
if not validated_doc:
|
||||
continue
|
||||
|
||||
link_count += 1
|
||||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"docstatus": link.docstatus,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
|
||||
# sort linked documents by ascending number of links
|
||||
docs.sort(key=lambda doc: doc.get("link_count"))
|
||||
return {
|
||||
"docs": docs,
|
||||
"count": link_count
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_all_linked_docs(docs):
|
||||
"""
|
||||
Cancel all linked doctype
|
||||
|
||||
Arguments:
|
||||
docs (str) - It contains all list of dictionaries of a linked documents.
|
||||
"""
|
||||
|
||||
docs = json.loads(docs)
|
||||
for i, doc in enumerate(docs, 1):
|
||||
if validate_linked_doc(doc) is True:
|
||||
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
|
||||
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
|
||||
linked_doc.cancel()
|
||||
|
||||
|
||||
def validate_linked_doc(docinfo):
|
||||
"""
|
||||
Validate a document to be submitted and non-exempted from auto-cancel.
|
||||
|
||||
Args:
|
||||
docs (dict): The document to check for submitted and non-exempt from auto-cancel
|
||||
|
||||
Returns:
|
||||
bool: True if linked document passes all validations, else False
|
||||
"""
|
||||
|
||||
# skip non-submittable doctypes since they don't need to be cancelled
|
||||
if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
|
||||
return False
|
||||
|
||||
# skip draft or cancelled documents
|
||||
if docinfo.get('docstatus') != 1:
|
||||
return False
|
||||
|
||||
# skip other doctypes since they don't need to be cancelled
|
||||
auto_cancel_exempt_doctypes = get_exempted_doctypes()
|
||||
if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_exempted_doctypes():
|
||||
""" Get list of doctypes exempted from being auto-cancelled """
|
||||
|
||||
auto_cancel_exempt_doctypes = []
|
||||
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
|
||||
auto_cancel_exempt_doctypes.append(doctypes)
|
||||
return auto_cancel_exempt_doctypes
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
|
|||
if is_single(df.doctype): continue
|
||||
|
||||
# optimized to get both link exists and parenttype
|
||||
possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype
|
||||
from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True)
|
||||
possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype},
|
||||
fields=['parenttype'], distinct=True)
|
||||
|
||||
if not possible_link: continue
|
||||
|
||||
|
|
@ -203,4 +308,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
|
|||
"doctype_fieldname": df.doctype_fieldname
|
||||
}
|
||||
|
||||
return ret
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ class Leaderboard {
|
|||
render_search_box() {
|
||||
|
||||
this.$search_box =
|
||||
$(`<div class="leaderboard-search col-md-3">
|
||||
$(`<div class="leaderboard-search form-group col-md-3">
|
||||
<input type="text" placeholder="Search" class="form-control leaderboard-search-input input-sm">
|
||||
</div>`);
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ class Leaderboard {
|
|||
|
||||
const link = `#Form/${this.options.selected_doctype}/${item.name}`;
|
||||
const name_html = item.formatted_name ?
|
||||
`<span class="text-muted ellipsis">${item.formatted_name}</span>`
|
||||
`<span class="text-muted ellipsis list-id">${item.formatted_name}</span>`
|
||||
: `<a class="grey list-id ellipsis" href="${link}"> ${item.name} </a>`;
|
||||
const html =
|
||||
`<div class="list-item">
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, os, base64
|
||||
from frappe import safe_decode
|
||||
from frappe.email.receive import Email
|
||||
from frappe.email.email_body import (replace_filename_with_cid,
|
||||
get_email, inline_style_in_html, get_header)
|
||||
get_email, inline_style_in_html, get_header)
|
||||
from frappe.email.queue import prepare_message, get_email_queue
|
||||
from six import PY3
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ This is the text version of this email
|
|||
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
self.assertTrue("<h1>=EA=80=80abcd=DE=B4</h1>" in result)
|
||||
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)
|
||||
|
||||
def test_prepare_message_returns_cr_lf(self):
|
||||
email = get_email_queue(
|
||||
|
|
@ -67,7 +68,8 @@ This is the text version of this email
|
|||
content='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
formatted='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
result = safe_decode(prepare_message(email=email,
|
||||
recipient='test@test.com', recipients_list=[]))
|
||||
if PY3:
|
||||
self.assertTrue(result.count('\n') == result.count("\r"))
|
||||
else:
|
||||
|
|
@ -81,9 +83,10 @@ This is the text version of this email
|
|||
subject='Test Subject',
|
||||
content='<h1>Whatever</h1>',
|
||||
text_content='whatever',
|
||||
message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti")
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti")
|
||||
result = safe_decode(prepare_message(email=email, recipient='test@test.com',
|
||||
recipients_list=[]))
|
||||
self.assertTrue(
|
||||
"a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti" in result)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2017-09-08 16:16:13.060641",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sb_doc_events",
|
||||
"naming_series",
|
||||
"webhook_doctype",
|
||||
"cb_doc_events",
|
||||
"webhook_docevent",
|
||||
|
|
@ -43,6 +46,7 @@
|
|||
{
|
||||
"fieldname": "webhook_docevent",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Doc Event",
|
||||
"options": "after_insert\non_update\non_submit\non_cancel\non_trash\non_update_after_submit\non_change",
|
||||
"set_only_once": 1
|
||||
|
|
@ -117,9 +121,16 @@
|
|||
"fieldname": "webhook_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "JSON Request Body"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"options": "\nHOOK-.####"
|
||||
}
|
||||
],
|
||||
"modified": "2019-08-26 00:38:14.611267",
|
||||
"links": [],
|
||||
"modified": "2020-01-06 02:51:07.997566",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Webhook",
|
||||
|
|
@ -140,5 +151,6 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "webhook_doctype",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -18,9 +18,6 @@ from frappe.utils.jinja import validate_template
|
|||
|
||||
|
||||
class Webhook(Document):
|
||||
def autoname(self):
|
||||
self.name = self.webhook_doctype + "-" + self.webhook_docevent
|
||||
|
||||
def validate(self):
|
||||
self.validate_docevent()
|
||||
self.validate_condition()
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ def login_via_office365(code, state):
|
|||
def login_via_salesforce(code, state):
|
||||
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_fairlogin(code, state):
|
||||
login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def custom(code, state):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -648,18 +648,96 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
savecancel(btn, callback, on_error) {
|
||||
var me = this;
|
||||
|
||||
const me = this;
|
||||
this.validate_form_action('Cancel');
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() {
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
|
||||
args: {
|
||||
doctype: me.doc.doctype,
|
||||
name: me.doc.name
|
||||
},
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message.count > 0) {
|
||||
me._cancel_all(r, btn, callback, on_error);
|
||||
} else {
|
||||
me._cancel(btn, callback, on_error, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_cancel_all(r, btn, callback, on_error) {
|
||||
const me = this;
|
||||
|
||||
// add confirmation message for cancelling all linked docs
|
||||
let links_text = "";
|
||||
let links = r.message.docs;
|
||||
const doctypes = Array.from(new Set(links.map(link => link.doctype)));
|
||||
|
||||
for (let doctype of doctypes) {
|
||||
let docnames = links
|
||||
.filter((link) => link.doctype == doctype)
|
||||
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
|
||||
.join(", ");
|
||||
links_text += `<li><strong>${doctype}</strong>: ${docnames}</li>`;
|
||||
}
|
||||
links_text = `<ul>${links_text}</ul>`;
|
||||
|
||||
let confirm_message = __('{0} {1} is linked with the following submitted documents: {2}',
|
||||
[(me.doc.doctype).bold(), me.doc.name, links_text]);
|
||||
|
||||
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
|
||||
if (can_cancel) {
|
||||
confirm_message += __('Do you want to cancel all linked documents?');
|
||||
} else {
|
||||
confirm_message += __('You do not have permissions to cancel all linked documents.');
|
||||
}
|
||||
|
||||
// generate dialog box to cancel all linked docs
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Cancel All Documents"),
|
||||
fields: [{
|
||||
fieldtype: "HTML",
|
||||
options: `<p class="frappe-confirm-message">${confirm_message}</p>`
|
||||
}]
|
||||
}, () => me.handle_save_fail(btn, on_error));
|
||||
|
||||
// if user can cancel all linked docs, add action to the dialog
|
||||
if (can_cancel) {
|
||||
d.set_primary_action("Cancel All", () => {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
|
||||
args: {
|
||||
docs: links
|
||||
},
|
||||
freeze: true,
|
||||
callback: (resp) => {
|
||||
if (!resp.exc) {
|
||||
me.reload_doc();
|
||||
me._cancel(btn, callback, on_error, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
d.show();
|
||||
};
|
||||
|
||||
_cancel(btn, callback, on_error, skip_confirm) {
|
||||
const me = this;
|
||||
const cancel_doc = () => {
|
||||
frappe.validated = true;
|
||||
me.script_manager.trigger("before_cancel").then(function() {
|
||||
if(!frappe.validated) {
|
||||
me.script_manager.trigger("before_cancel").then(() => {
|
||||
if (!frappe.validated) {
|
||||
return me.handle_save_fail(btn, on_error);
|
||||
}
|
||||
|
||||
var after_cancel = function(r) {
|
||||
if(r.exc) {
|
||||
if (r.exc) {
|
||||
me.handle_save_fail(btn, on_error);
|
||||
} else {
|
||||
frappe.utils.play_sound("cancel");
|
||||
|
|
@ -670,8 +748,14 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
};
|
||||
frappe.ui.form.save(me, "cancel", after_cancel, btn);
|
||||
});
|
||||
}, () => me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
}
|
||||
|
||||
if (skip_confirm) {
|
||||
cancel_doc();
|
||||
} else {
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
};
|
||||
|
||||
savetrash() {
|
||||
this.validate_form_action("Delete");
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export default class Grid {
|
|||
this.remove_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
|
||||
this.remove_all_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true);
|
||||
}
|
||||
|
||||
get_selected() {
|
||||
|
|
@ -426,7 +426,7 @@ export default class Grid {
|
|||
}
|
||||
},
|
||||
onUpdate: (event) => {
|
||||
let idx = $(event.item).closest('.grid-row').attr('data-idx');
|
||||
let idx = $(event.item).closest('.grid-row').attr('data-idx') - 1;
|
||||
let doc = this.data[idx%this.grid_pagination.page_length];
|
||||
this.renumber_based_on_dom();
|
||||
this.frm.script_manager.trigger(this.df.fieldname + "_move", this.df.options, doc.name);
|
||||
|
|
|
|||
|
|
@ -69,16 +69,65 @@ frappe.ui.form.Toolbar = Class.extend({
|
|||
can_rename: function() {
|
||||
return this.frm.perm[0].write && this.frm.meta.allow_rename && !this.frm.doc.__islocal;
|
||||
},
|
||||
show_unchanged_document_alert: function() {
|
||||
frappe.show_alert({
|
||||
indicator: "yellow",
|
||||
message: __("Unchanged")
|
||||
});
|
||||
},
|
||||
rename_document_title(new_name, new_title, merge=false) {
|
||||
const docname = this.frm.doc.name;
|
||||
const title_field = this.frm.meta.title_field || '';
|
||||
const doctype = this.frm.doctype;
|
||||
|
||||
let confirm_message=null;
|
||||
|
||||
if (new_name) {
|
||||
const warning = __("This cannot be undone");
|
||||
const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]);
|
||||
confirm_message = `${message}<br><b>${warning}<b>`;
|
||||
}
|
||||
|
||||
let rename_document = () => {
|
||||
return frappe.xcall("frappe.model.rename_doc.update_document_title", {
|
||||
doctype,
|
||||
docname,
|
||||
new_name,
|
||||
title_field,
|
||||
old_title: this.frm.doc[title_field],
|
||||
new_title,
|
||||
merge
|
||||
}).then(new_docname => {
|
||||
if (new_name != docname) {
|
||||
$(document).trigger("rename", [doctype, docname, new_docname || new_name]);
|
||||
if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname];
|
||||
}
|
||||
this.frm.reload_doc();
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (new_title === this.frm.doc[title_field] && new_name === docname) {
|
||||
this.show_unchanged_document_alert();
|
||||
resolve();
|
||||
} else if (merge) {
|
||||
frappe.confirm(confirm_message, () => {
|
||||
rename_document().then(resolve).catch(reject);
|
||||
}, reject);
|
||||
} else {
|
||||
rename_document().then(resolve).catch(reject);
|
||||
}
|
||||
});
|
||||
},
|
||||
setup_editable_title: function () {
|
||||
let me = this;
|
||||
|
||||
this.page.$title_area.find(".title-text").on("click", () => {
|
||||
let fields = [];
|
||||
let doctype = me.frm.doctype;
|
||||
let docname = me.frm.doc.name;
|
||||
let title_field = me.frm.meta.title_field || '';
|
||||
|
||||
// check if title is updateable
|
||||
// check if title is updatable
|
||||
if (me.is_title_editable()) {
|
||||
let title_field_label = me.frm.get_docfield(title_field).label;
|
||||
|
||||
|
|
@ -91,7 +140,7 @@ frappe.ui.form.Toolbar = Class.extend({
|
|||
});
|
||||
}
|
||||
|
||||
// check if docname is updateable
|
||||
// check if docname is updatable
|
||||
if (me.can_rename()) {
|
||||
fields.push(...[{
|
||||
label: __("New Name"),
|
||||
|
|
@ -114,36 +163,15 @@ frappe.ui.form.Toolbar = Class.extend({
|
|||
fields: fields
|
||||
});
|
||||
d.show();
|
||||
|
||||
d.set_primary_action(__("Rename"), function () {
|
||||
let args = d.get_values();
|
||||
if (args.title != me.frm.doc[title_field] || args.name != docname) {
|
||||
frappe.call({
|
||||
method: "frappe.model.rename_doc.update_document_title",
|
||||
args: {
|
||||
doctype,
|
||||
docname,
|
||||
title_field,
|
||||
old_title: me.frm.doc[title_field],
|
||||
new_title: args.title,
|
||||
new_name: args.name,
|
||||
merge: args.merge
|
||||
},
|
||||
btn: d.get_primary_btn()
|
||||
}).then((res) => {
|
||||
me.frm.reload_doc();
|
||||
if (!res.exc && (args.name != docname)) {
|
||||
$(document).trigger("rename", [doctype, docname, res.message || args.name]);
|
||||
if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname];
|
||||
}
|
||||
d.set_primary_action(__("Rename"), (values) => {
|
||||
d.disable_primary_action();
|
||||
this.rename_document_title(values.name, values.title, values.merge)
|
||||
.then(() => {
|
||||
d.hide();
|
||||
})
|
||||
.catch(() => {
|
||||
d.enable_primary_action();
|
||||
});
|
||||
} else {
|
||||
frappe.show_alert({
|
||||
indicator: "yellow",
|
||||
message: __("Unchanged")
|
||||
});
|
||||
}
|
||||
d.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -335,14 +335,13 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
field: field,
|
||||
stat: stats,
|
||||
sum: sum,
|
||||
label: field === '_user_tags' ? (tags ? __(label) : __("Tag")) : __(label),
|
||||
label: field === '_user_tags' ? (tags ? __(label) : __("Tags")) : __(label),
|
||||
};
|
||||
$(frappe.render_template("list_sidebar_stat", context))
|
||||
.on("click", ".stat-link", function() {
|
||||
var doctype = "Tag Link";
|
||||
var fieldname = $(this).attr('data-field');
|
||||
var label = $(this).attr('data-label');
|
||||
var condition = "=";
|
||||
var condition = "like";
|
||||
var existing = me.list_view.filter_area.filter_list.get_filter(fieldname);
|
||||
if(existing) {
|
||||
existing.remove();
|
||||
|
|
@ -351,7 +350,7 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
label = "%,%";
|
||||
condition = "not like";
|
||||
}
|
||||
me.list_view.filter_area.filter_list.add_filter(doctype, fieldname, condition, label)
|
||||
me.list_view.filter_area.filter_list.add_filter(me.list_view.doctype, fieldname, condition, label)
|
||||
.then(function() {
|
||||
me.list_view.refresh();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
tag_editor.wrapper.on('click', '.tagit-label', (e) => {
|
||||
const $this = $(e.currentTarget);
|
||||
this.filter_area.add('Tag Link', 'tag', '=', $this.text());
|
||||
this.filter_area.add(this.doctype, '_user_tags', '=', $this.text());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -550,23 +550,28 @@ $.extend(frappe.model, {
|
|||
},
|
||||
|
||||
rename_doc: function(doctype, docname, callback) {
|
||||
let message = __("Merge with existing");
|
||||
let warning = __("This cannot be undone");
|
||||
let merge_label = message + " <b>(" + warning + ")</b>";
|
||||
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Rename {0}", [__(docname)]),
|
||||
fields: [
|
||||
{label:__("New Name"), fieldname: "new_name", fieldtype:"Data", reqd:1, "default": docname},
|
||||
{label:__("Merge with existing"), fieldtype:"Check", fieldname:"merge"},
|
||||
{label: __("New Name"), fieldname: "new_name", fieldtype: "Data", reqd: 1, "default": docname},
|
||||
{label: merge_label, fieldtype: "Check", fieldname: "merge"},
|
||||
]
|
||||
});
|
||||
|
||||
d.set_primary_action(__("Rename"), function() {
|
||||
var args = d.get_values();
|
||||
if(!args) return;
|
||||
return frappe.call({
|
||||
method:"frappe.model.rename_doc.rename_doc",
|
||||
method:"frappe.rename_doc",
|
||||
args: {
|
||||
doctype: doctype,
|
||||
old: docname,
|
||||
"new": args.new_name,
|
||||
"merge": args.merge
|
||||
new: args.new_name,
|
||||
merge: args.merge
|
||||
},
|
||||
btn: d.get_primary_btn(),
|
||||
callback: function(r,rt) {
|
||||
|
|
|
|||
|
|
@ -174,11 +174,6 @@ frappe.ui.Filter = class {
|
|||
|
||||
let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname];
|
||||
|
||||
if (doctype === "Tag Link" || fieldname === "_user_tags") {
|
||||
original_docfield = {fieldname: "tag", fieldtype: "Data", label: "Tags", parent: "Tag Link"};
|
||||
doctype = "Tag Link";
|
||||
}
|
||||
|
||||
if(!original_docfield) {
|
||||
console.warn(`Field ${fieldname} is not selectable.`);
|
||||
this.remove();
|
||||
|
|
|
|||
|
|
@ -63,11 +63,6 @@ frappe.ui.FilterGroup = class {
|
|||
}
|
||||
|
||||
validate_args(doctype, fieldname) {
|
||||
// Tags attached to the document are maintained seperately in Tag Link
|
||||
// and is not the part of doctype meta therefore tag fieldname validation is ignored.
|
||||
if (doctype === "Tag Link" && fieldname === "tag") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(doctype && fieldname
|
||||
&& !frappe.meta.has_field(doctype, fieldname)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
});
|
||||
}
|
||||
|
||||
get_fields() {
|
||||
this.fields.push([this.board.field_name, this.board.reference_doctype]);
|
||||
return super.get_fields();
|
||||
}
|
||||
|
||||
render() {
|
||||
const board_name = this.board_name;
|
||||
if (this.kanban && board_name === this.kanban.board_name) {
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@
|
|||
<!-- body -->
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
<tr>
|
||||
<tr style="height: 30px">
|
||||
{% for col in columns %}
|
||||
{% if col.name && col._id !== "_check" %}
|
||||
|
||||
{% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %}
|
||||
|
||||
<td>
|
||||
<td {% if row.bold == 1 %} style="font-weight: bold" {% endif %}>
|
||||
<span {% if col._index == 0 %} style="padding-left: {%= cint(row.indent) * 2 %}em" {% endif %}>
|
||||
{{
|
||||
col.formatter
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
let report_script_name = this.report_doc.report_type === 'Custom Report'
|
||||
? this.report_doc.reference_report
|
||||
: this.report_name;
|
||||
return frappe.query_reports[report_script_name];
|
||||
return frappe.query_reports[report_script_name] || {};
|
||||
}
|
||||
|
||||
setup_progress_bar() {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,11 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
}
|
||||
|
||||
setup_delete_button() {
|
||||
this.add_button_to_header("Delete", "danger", () => this.delete());
|
||||
this.add_button_to_header(
|
||||
'<i class="fa fa-trash" aria-hidden="true"></i>',
|
||||
"light",
|
||||
() => this.delete()
|
||||
);
|
||||
}
|
||||
|
||||
setup_print_button() {
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export default class WebFormList {
|
|||
() => (window.location.href = window.location.pathname + "?new=1")
|
||||
);
|
||||
|
||||
if (this.rows.length <= this.page_length) {
|
||||
if (this.rows.length > this.page_length) {
|
||||
addButton(footer, "more", "secondary", false, "More", () => this.more());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ login.bind_events = function() {
|
|||
}
|
||||
});
|
||||
|
||||
{% if ldap_settings.enabled %}
|
||||
{% if ldap_settings and ldap_settings.enabled %}
|
||||
$(".btn-ldap-login").on("click", function(){
|
||||
var args = {};
|
||||
args.cmd = "{{ ldap_settings.method }}";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<nav class="navbar navbar-light bg-white navbar-expand-lg sticky-top shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_prefix }}{{ home_page or "/" }}">
|
||||
<span>{{ brand_html or (frappe.get_hooks("brand_html") or ["Home"])[0] }}</span>
|
||||
<span>{{ brand_html or (frappe.get_hooks("brand_html") or [_("Home")])[0] }}</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button"
|
||||
data-toggle="collapse"
|
||||
|
|
@ -17,4 +17,4 @@
|
|||
{% include "templates/includes/navbar/navbar_items.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if frappe.form_dict.scope %}
|
||||
<input type="text" hidden name="scope" value="{{ frappe.form_dict.scope }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 -*-
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue