Merge branch 'develop' of https://github.com/frappe/frappe into paytm-integration
This commit is contained in:
commit
eefa54bcaa
69 changed files with 1934 additions and 627 deletions
|
|
@ -9,14 +9,14 @@ context('Relative Timeframe', () => {
|
|||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('set relative filter for Previous and check list', () => {
|
||||
it('sets relative timespan filter for last week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Previous");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
|
|
@ -28,13 +28,13 @@ context('Relative Timeframe', () => {
|
|||
cy.get('.remove-filter.btn').click();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
it('set relative filter for Next and check list', () => {
|
||||
it('sets relative timespan filter for next week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Next");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
|
|
@ -231,9 +231,8 @@ def get_site_config(sites_path=None, site_path=None):
|
|||
if os.path.exists(site_config):
|
||||
config.update(get_file_json(site_config))
|
||||
elif local.site and not local.flags.new_site:
|
||||
print("{0} does not exist".format(local.site))
|
||||
print("Site {0} does not exist".format(local.site))
|
||||
sys.exit(1)
|
||||
#raise IncorrectSitePath, "{0} does not exist".format(site_config)
|
||||
|
||||
return _dict(config)
|
||||
|
||||
|
|
@ -1559,10 +1558,10 @@ def get_doctype_app(doctype):
|
|||
|
||||
loggers = {}
|
||||
log_level = None
|
||||
def logger(module=None, with_more_info=True):
|
||||
def logger(module=None, with_more_info=False):
|
||||
'''Returns a python logger that uses StreamHandler'''
|
||||
from frappe.utils.logger import get_logger
|
||||
return get_logger(module or 'default', with_more_info=with_more_info)
|
||||
return get_logger(module=module, with_more_info=with_more_info)
|
||||
|
||||
def log_error(message=None, title=_("Error")):
|
||||
'''Log error to Error Log'''
|
||||
|
|
|
|||
|
|
@ -99,6 +99,16 @@ def application(request):
|
|||
frappe.monitor.stop(response)
|
||||
frappe.recorder.dump()
|
||||
|
||||
frappe.logger("web").info({
|
||||
"site": get_site_name(request.host),
|
||||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
|
||||
"base_url": getattr(request, "base_url", "NOTFOUND"),
|
||||
"full_path": getattr(request, "full_path", "NOTFOUND"),
|
||||
"method": getattr(request, "method", "NOTFOUND"),
|
||||
"scheme": getattr(request, "scheme", "NOTFOUND"),
|
||||
"http_status_code": getattr(response, "status_code", "NOTFOUND")
|
||||
})
|
||||
|
||||
if response and hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
|
|
@ -195,7 +205,6 @@ def handle_exception(e):
|
|||
frappe.local.login_manager.clear_cookies()
|
||||
|
||||
if http_status_code >= 500:
|
||||
frappe.logger().error('Request Error', exc_info=True)
|
||||
make_error_snapshot(e)
|
||||
|
||||
if return_as_message:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from frappe.email.inbox import get_email_accounts
|
|||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
|
||||
def get_bootinfo():
|
||||
|
|
@ -84,6 +85,7 @@ def get_bootinfo():
|
|||
bootinfo.points = get_energy_points(frappe.session.user)
|
||||
bootinfo.frequently_visited_links = frequently_visited_links()
|
||||
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
|
||||
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
|
@ -106,6 +108,7 @@ def load_desktop_data(bootinfo):
|
|||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
|
||||
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
def get_allowed_pages(cache=False):
|
||||
|
|
@ -295,3 +298,11 @@ def get_link_preview_doctypes():
|
|||
link_preview_doctypes.append(custom.doc_type)
|
||||
|
||||
return link_preview_doctypes
|
||||
|
||||
def get_additional_filters_from_hooks():
|
||||
filter_config = frappe._dict()
|
||||
filter_hooks = frappe.get_hooks('filters_config')
|
||||
for hook in filter_hooks:
|
||||
filter_config.update(frappe.get_attr(hook)())
|
||||
|
||||
return filter_config
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ def pass_context(f):
|
|||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
ret = f(frappe._dict(ctx.obj), *args, **kwargs)
|
||||
try:
|
||||
ret = f(frappe._dict(ctx.obj), *args, **kwargs)
|
||||
except frappe.exceptions.SiteNotSpecifiedError as e:
|
||||
click.secho(str(e), fg='yellow')
|
||||
sys.exit(1)
|
||||
|
||||
if profile:
|
||||
pr.disable()
|
||||
|
|
@ -44,8 +48,7 @@ def get_site(context):
|
|||
site = context.sites[0]
|
||||
return site
|
||||
except (IndexError, TypeError):
|
||||
print('Please specify --site sitename')
|
||||
sys.exit(1)
|
||||
raise frappe.SiteNotSpecifiedError
|
||||
|
||||
def popen(command, *args, **kwargs):
|
||||
output = kwargs.get('output', True)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import sys
|
|||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
|
||||
def _is_scheduler_enabled():
|
||||
enable_scheduler = False
|
||||
|
|
@ -30,6 +31,8 @@ def trigger_scheduler_event(context, event):
|
|||
frappe.utils.scheduler.trigger(site, event, now=True)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('enable-scheduler')
|
||||
@pass_context
|
||||
|
|
@ -45,6 +48,8 @@ def enable_scheduler(context):
|
|||
print("Enabled for", site)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('disable-scheduler')
|
||||
@pass_context
|
||||
|
|
@ -60,7 +65,8 @@ def disable_scheduler(context):
|
|||
print("Disabled for", site)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('scheduler')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import get_site_path, touch_file
|
||||
|
||||
|
|
@ -130,30 +131,47 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
|
||||
if not os.path.exists(sql_file_path):
|
||||
sql_file_path = '../' + sql_file_path
|
||||
base_path = '..'
|
||||
sql_file_path = os.path.join(base_path, sql_file_path)
|
||||
if not os.path.exists(sql_file_path):
|
||||
print('Invalid path {0}'.format(sql_file_path[3:]))
|
||||
sys.exit(1)
|
||||
elif sql_file_path.startswith(os.sep):
|
||||
base_path = os.sep
|
||||
else:
|
||||
base_path = '.'
|
||||
|
||||
|
||||
if sql_file_path.endswith('sql.gz'):
|
||||
sql_file_path = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
else:
|
||||
decompressed_file_name = sql_file_path
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=sql_file_path,
|
||||
force=context.force)
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True)
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
with_public_files = os.path.join(base_path, with_public_files)
|
||||
public = extract_tar_files(site, with_public_files, 'public')
|
||||
os.remove(public)
|
||||
|
||||
if with_private_files:
|
||||
with_private_files = os.path.join(base_path, with_private_files)
|
||||
private = extract_tar_files(site, with_private_files, 'private')
|
||||
os.remove(private)
|
||||
|
||||
# Removing temporarily created file
|
||||
if decompressed_file_name != sql_file_path:
|
||||
os.remove(decompressed_file_name)
|
||||
|
||||
success_message = "Site {0} has been restored{1}".format(site, " with files" if (with_public_files or with_private_files) else "")
|
||||
click.secho(success_message, fg="green")
|
||||
|
||||
@click.command('reinstall')
|
||||
@click.option('--admin-password', help='Administrator Password for reinstalled site')
|
||||
@click.option('--mariadb-root-username', help='Root username for MariaDB')
|
||||
|
|
@ -200,6 +218,8 @@ def install_app(context, apps):
|
|||
_install_app(app, verbose=context.verbose)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('list-apps')
|
||||
@pass_context
|
||||
|
|
@ -229,7 +249,8 @@ def add_system_manager(context, email, first_name, last_name, send_welcome_email
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('disable-user')
|
||||
@click.argument('email')
|
||||
|
|
@ -260,6 +281,8 @@ def migrate(context, rebuild_website=False, skip_failing=False):
|
|||
migrate(context.verbose, rebuild_website=rebuild_website, skip_failing=skip_failing)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
print("Compiling Python Files...")
|
||||
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
|
||||
|
|
@ -271,7 +294,12 @@ def migrate_to(context, frappe_provider):
|
|||
"Migrates site to the specified provider"
|
||||
from frappe.integrations.frappe_providers import migrate_to
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
migrate_to(site, frappe_provider)
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('run-patch')
|
||||
@click.argument('module')
|
||||
|
|
@ -286,6 +314,8 @@ def run_patch(context, module):
|
|||
frappe.modules.patch_handler.run_single(module, force=context.force)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('reload-doc')
|
||||
@click.argument('module')
|
||||
|
|
@ -302,6 +332,8 @@ def reload_doc(context, module, doctype, docname):
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('reload-doctype')
|
||||
@click.argument('doctype')
|
||||
|
|
@ -316,6 +348,8 @@ def reload_doctype(context, doctype):
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('add-to-hosts')
|
||||
@pass_context
|
||||
|
|
@ -323,6 +357,8 @@ def add_to_hosts(context):
|
|||
"Add site to hosts"
|
||||
for site in context.sites:
|
||||
frappe.commands.popen('echo 127.0.0.1\t{0} | sudo tee -a /etc/hosts'.format(site))
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('use')
|
||||
@click.argument('site')
|
||||
|
|
@ -336,7 +372,7 @@ def use(site, sites_path='.'):
|
|||
sitefile.write(site)
|
||||
print("Current Site set to {}".format(site))
|
||||
else:
|
||||
print("{} does not exist".format(site))
|
||||
print("Site {} does not exist".format(site))
|
||||
|
||||
@click.command('backup')
|
||||
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
|
||||
|
|
@ -369,6 +405,9 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
print("Private files: ", odb.backup_path_private_files)
|
||||
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
@click.command('remove-from-installed-apps')
|
||||
|
|
@ -384,6 +423,8 @@ def remove_from_installed_apps(context, app):
|
|||
remove_from_installed_apps(app)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('uninstall-app')
|
||||
@click.argument('app')
|
||||
|
|
@ -400,6 +441,8 @@ def uninstall(context, app, dry_run=False, yes=False):
|
|||
remove_app(app, dry_run, yes)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('drop-site')
|
||||
|
|
@ -430,7 +473,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
|||
else:
|
||||
click.echo("="*80)
|
||||
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
|
||||
click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n"))
|
||||
click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n"))
|
||||
click.echo("Fix the issue and try again.")
|
||||
click.echo(
|
||||
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
|
||||
|
|
@ -491,6 +534,8 @@ def set_admin_password(context, admin_password, logout_all_sessions=False):
|
|||
admin_password = None
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('set-last-active-for-user')
|
||||
@click.option('--user', help="Setup last active date for user")
|
||||
|
|
@ -536,6 +581,8 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('browse')
|
||||
@click.argument('site', required=False)
|
||||
|
|
@ -563,6 +610,8 @@ def start_recording(context):
|
|||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.recorder.start()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('stop-recording')
|
||||
|
|
@ -571,6 +620,8 @@ def stop_recording(context):
|
|||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.recorder.stop()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
commands = [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals, absolute_import, print_function
|
||||
import click
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
|
||||
# translation
|
||||
@click.command('build-message-files')
|
||||
|
|
@ -15,6 +16,8 @@ def build_message_files(context):
|
|||
frappe.translate.rebuild_all_translation_files()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('new-language') #, help="Create lang-code.csv for given app")
|
||||
@pass_context
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import json, os, sys, subprocess
|
|||
from distutils.spawn import find_executable
|
||||
import frappe
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.utils import update_progress_bar, get_bench_path
|
||||
from frappe.utils.response import json_handler
|
||||
from coverage import Coverage
|
||||
|
|
@ -51,7 +52,8 @@ def clear_cache(context):
|
|||
frappe.website.render.clear_cache()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('clear-website-cache')
|
||||
@pass_context
|
||||
|
|
@ -65,7 +67,8 @@ def clear_website_cache(context):
|
|||
frappe.website.render.clear_cache()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('destroy-all-sessions')
|
||||
@click.option('--reason')
|
||||
|
|
@ -81,7 +84,8 @@ def destroy_all_sessions(context, reason=None):
|
|||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('show-config')
|
||||
@pass_context
|
||||
|
|
@ -117,7 +121,8 @@ def reset_perms(context):
|
|||
reset_perms(d)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('execute')
|
||||
@click.argument('method')
|
||||
|
|
@ -164,6 +169,9 @@ def execute(context, method, args=None, kwargs=None, profile=False):
|
|||
if ret:
|
||||
print(json.dumps(ret, default=json_handler))
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('add-to-email-queue')
|
||||
@click.argument('email-path')
|
||||
|
|
@ -197,7 +205,8 @@ def export_doc(context, doctype, docname):
|
|||
frappe.modules.export_doc(doctype, docname)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('export-json')
|
||||
@click.argument('doctype')
|
||||
|
|
@ -214,7 +223,8 @@ def export_json(context, doctype, path, name=None):
|
|||
data_import.export_json(doctype, path, name=name)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('export-csv')
|
||||
@click.argument('doctype')
|
||||
|
|
@ -230,7 +240,8 @@ def export_csv(context, doctype, path):
|
|||
data_import.export_csv(doctype, path)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('export-fixtures')
|
||||
@click.option('--app', default=None, help='Export fixtures of a specific app')
|
||||
|
|
@ -245,7 +256,8 @@ def export_fixtures(context, app=None):
|
|||
export_fixtures(app=app)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('import-doc')
|
||||
@click.argument('path')
|
||||
|
|
@ -267,7 +279,8 @@ def import_doc(context, path, force=False):
|
|||
data_import.import_doc(path, overwrite=context.force)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('import-csv')
|
||||
@click.argument('path')
|
||||
|
|
@ -364,6 +377,8 @@ def mariadb(context):
|
|||
import os
|
||||
|
||||
site = get_site(context)
|
||||
if not site:
|
||||
raise SiteNotSpecifiedError
|
||||
frappe.init(site=site)
|
||||
|
||||
# This is assuming you're within the bench instance.
|
||||
|
|
@ -577,7 +592,8 @@ def request(context, args=None, path=None):
|
|||
print(frappe.response)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('make-app')
|
||||
@click.argument('destination')
|
||||
|
|
@ -658,7 +674,8 @@ def rebuild_global_search(context, static_pages=False):
|
|||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('auto-deploy')
|
||||
@click.argument('app')
|
||||
|
|
|
|||
|
|
@ -158,11 +158,7 @@ class TestAccessLog(unittest.TestCase):
|
|||
request = requests.post(private_file_link, headers=self.header)
|
||||
last_doc = frappe.get_last_doc('Access Log')
|
||||
|
||||
if request.status_code == 403:
|
||||
# if file is not accessible, access log wont be generated
|
||||
pass
|
||||
|
||||
else:
|
||||
if request.ok:
|
||||
# check for the access log of downloaded file
|
||||
self.assertEqual(new_private_file.doctype, last_doc.export_from)
|
||||
self.assertEqual(new_private_file.name, last_doc.reference_document)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ class File(Document):
|
|||
def before_insert(self):
|
||||
frappe.local.rollback_observers.append(self)
|
||||
self.set_folder_name()
|
||||
if self.file_name:
|
||||
self.file_name = re.sub(r'/', '', self.file_name)
|
||||
self.content = self.get("content", None)
|
||||
self.decode = self.get("decode", False)
|
||||
if self.content:
|
||||
|
|
@ -192,6 +194,8 @@ class File(Document):
|
|||
def set_file_name(self):
|
||||
if not self.file_name and self.file_url:
|
||||
self.file_name = self.file_url.split('/')[-1]
|
||||
else:
|
||||
self.file_name = re.sub(r'/', '', self.file_name)
|
||||
|
||||
def generate_content_hash(self):
|
||||
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
|
||||
|
|
@ -405,6 +409,12 @@ class File(Document):
|
|||
frappe.throw(_("URL must start with 'http://' or 'https://'"))
|
||||
return
|
||||
|
||||
if not self.file_url.startswith(("http://", "https://")):
|
||||
# local file
|
||||
root_files_path = get_files_path(is_private=self.is_private)
|
||||
if not os.path.commonpath([root_files_path]) == os.path.commonpath([root_files_path, self.get_full_path()]):
|
||||
# basically the file url is skewed to not point to /files/ or /private/files
|
||||
frappe.throw(_("{0} is not a valid file url").format(self.file_url))
|
||||
self.file_url = unquote(self.file_url)
|
||||
self.file_size = frappe.form_dict.file_size or self.file_size
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class ScheduledJobType(Document):
|
|||
|
||||
def log_status(self, status):
|
||||
# log file
|
||||
frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
|
||||
frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
|
||||
self.update_scheduler_log(status)
|
||||
|
||||
def update_scheduler_log(self, status):
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ class DeskPage(Document):
|
|||
if frappe.conf.developer_mode and self.is_standard:
|
||||
export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module)
|
||||
|
||||
@staticmethod
|
||||
def get_module_page_map():
|
||||
filters = {
|
||||
'extends_another_page': 0,
|
||||
'for_user': '',
|
||||
}
|
||||
|
||||
pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1)
|
||||
|
||||
return { page[1]: page[0] for page in pages }
|
||||
|
||||
def disable_saving_as_standard():
|
||||
return frappe.flags.in_install or \
|
||||
frappe.flags.in_patch or \
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ class TestEvent(unittest.TestCase):
|
|||
|
||||
self.assertEqual(set(json.loads(ev._assign)), set(["test@example.com", self.test_user]))
|
||||
|
||||
# close an assignment
|
||||
# Remove an assignment
|
||||
todo = frappe.get_doc("ToDo", {"reference_type": ev.doctype, "reference_name": ev.name,
|
||||
"owner": self.test_user})
|
||||
todo.status = "Closed"
|
||||
todo.status = "Cancelled"
|
||||
todo.save()
|
||||
|
||||
ev = frappe.get_doc("Event", ev.name)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('List View Setting', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "Prompt",
|
||||
"beta": 0,
|
||||
"creation": "2019-03-06 13:29:21.101860",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "disable_count",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Disable Count",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "disable_sidebar_stats",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Disable Sidebar Stats",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "disable_auto_refresh",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Disable Auto Refresh",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-06 13:40:59.533586",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "List View Setting",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ListViewSetting(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('List View Settings', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-10-23 15:00:48.392374",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"disable_count",
|
||||
"disable_sidebar_stats",
|
||||
"disable_auto_refresh",
|
||||
"total_fields",
|
||||
"fields_html",
|
||||
"fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_count",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Count"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_sidebar_stats",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Sidebar Stats"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_auto_refresh",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Auto Refresh"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_fields",
|
||||
"fieldtype": "Select",
|
||||
"label": "Maximum Number of Fields",
|
||||
"options": "\n4\n5\n6\n7\n8\n9\n10"
|
||||
},
|
||||
{
|
||||
"fieldname": "fields_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "fields",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Fields",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-12 18:27:15.568199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "List View Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
79
frappe/desk/doctype/list_view_settings/list_view_settings.py
Normal file
79
frappe/desk/doctype/list_view_settings/list_view_settings.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ListViewSettings(Document):
|
||||
|
||||
def on_update(self):
|
||||
frappe.clear_document_cache(self.doctype, self.name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_listview_settings(doctype, listview_settings, removed_listview_fields):
|
||||
|
||||
listview_settings = frappe.parse_json(listview_settings)
|
||||
removed_listview_fields = frappe.parse_json(removed_listview_fields)
|
||||
|
||||
if frappe.get_all("List View Settings", filters={"name": doctype}):
|
||||
doc = frappe.get_doc("List View Settings", doctype)
|
||||
doc.update(listview_settings)
|
||||
doc.save()
|
||||
else:
|
||||
doc = frappe.new_doc("List View Settings")
|
||||
doc.name = doctype
|
||||
doc.update(listview_settings)
|
||||
doc.insert()
|
||||
|
||||
set_listview_fields(doctype, listview_settings.get("fields"), removed_listview_fields)
|
||||
|
||||
return {
|
||||
"meta": frappe.get_meta(doctype, False),
|
||||
"listview_settings": doc
|
||||
}
|
||||
|
||||
def set_listview_fields(doctype, listview_fields, removed_listview_fields):
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
listview_fields = [f.get("fieldname") for f in frappe.parse_json(listview_fields) if f.get("fieldname")]
|
||||
|
||||
for field in removed_listview_fields:
|
||||
set_in_list_view_property(doctype, meta.get_field(field), "0")
|
||||
|
||||
for field in listview_fields:
|
||||
set_in_list_view_property(doctype, meta.get_field(field), "1")
|
||||
|
||||
def set_in_list_view_property(doctype, field, value):
|
||||
if not field or field.fieldname == "status_field":
|
||||
return
|
||||
|
||||
property_setter = frappe.db.get_value("Property Setter", {"doc_type": doctype, "field_name": field.fieldname, "property": "in_list_view"})
|
||||
if property_setter:
|
||||
doc = frappe.get_doc("Property Setter", property_setter)
|
||||
doc.value = value
|
||||
doc.save()
|
||||
else:
|
||||
frappe.make_property_setter({
|
||||
"doctype": doctype,
|
||||
"doctype_or_field": "DocField",
|
||||
"fieldname": field.fieldname,
|
||||
"property": "in_list_view",
|
||||
"value": value,
|
||||
"property_type": "Check"
|
||||
}, ignore_validate=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_listview_fields(doctype):
|
||||
meta = frappe.get_meta(doctype)
|
||||
path = frappe.get_module_path(frappe.scrub(meta.module), "doctype", frappe.scrub(meta.name), frappe.scrub(meta.name) + ".json")
|
||||
doctype_json = frappe.get_file_json(path)
|
||||
|
||||
fields = [f.get("fieldname") for f in doctype_json.get("fields") if f.get("in_list_view")]
|
||||
|
||||
if meta.title_field:
|
||||
if not meta.title_field.strip() in fields:
|
||||
fields.append(meta.title_field.strip())
|
||||
|
||||
return fields
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestListViewSetting(unittest.TestCase):
|
||||
class TestListViewSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -3,10 +3,43 @@
|
|||
|
||||
frappe.ui.form.on('Notification Log', {
|
||||
refresh: function(frm) {
|
||||
let dt = frm.doc.document_type;
|
||||
let dn = frm.doc.document_name;
|
||||
frm.fields_dict.document_name.$input_wrapper
|
||||
.find('.control-value')
|
||||
.wrapInner(`<a href='#Form/${dt}/${dn}'></a>`);
|
||||
if (frm.doc.attached_file) {
|
||||
frm.trigger('set_attachment');
|
||||
} else {
|
||||
frm.get_field('attachment_link').$wrapper.empty();
|
||||
}
|
||||
},
|
||||
|
||||
open_reference_document: function(frm) {
|
||||
const dt = frm.doc.document_type;
|
||||
const dn = frm.doc.document_name;
|
||||
frappe.set_route('Form', dt, dn);
|
||||
},
|
||||
|
||||
set_attachment: function(frm) {
|
||||
const attachment = JSON.parse(frm.doc.attached_file);
|
||||
|
||||
const $wrapper = frm.get_field('attachment_link').$wrapper;
|
||||
$wrapper.html(`
|
||||
<div class="attached-file text-medium">
|
||||
<div class="ellipsis">
|
||||
<i class="fa fa-paperclip"></i>
|
||||
<a class="attached-file-link">${attachment.name}.pdf</a>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$wrapper.find(".attached-file-link").click(() => {
|
||||
const w = window.open(
|
||||
frappe.urllib.get_full_url(`/api/method/frappe.utils.print_format.download_pdf?
|
||||
doctype=${encodeURIComponent(attachment.doctype)}
|
||||
&name=${encodeURIComponent(attachment.name)}
|
||||
&format=${encodeURIComponent(attachment.print_format)}
|
||||
&lang=${encodeURIComponent(attachment.lang)}`)
|
||||
);
|
||||
if (!w) {
|
||||
frappe.msgprint(__("Please enable pop-ups"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2019-08-26 13:37:34.165254",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
|
|
@ -8,10 +9,12 @@
|
|||
"for_user",
|
||||
"type",
|
||||
"email_content",
|
||||
"column_break_4",
|
||||
"document_type",
|
||||
"read",
|
||||
"document_name",
|
||||
"attached_file",
|
||||
"attachment_link",
|
||||
"open_reference_document",
|
||||
"from_user"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -20,57 +23,65 @@
|
|||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"read_only": 1
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "for_user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "For User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Mention\nEnergy Point\nAssignment\nShare",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"options": "Mention\nEnergy Point\nAssignment\nShare\nAlert",
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email_content",
|
||||
"fieldtype": "Text",
|
||||
"label": "Email Content",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Message",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Document Name",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"hidden": 1,
|
||||
"label": "Document Link",
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "From User",
|
||||
"options": "User",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -78,26 +89,51 @@
|
|||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Read"
|
||||
"label": "Read",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "open_reference_document",
|
||||
"fieldtype": "Button",
|
||||
"label": "Open Reference Document",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "attached_file",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Attached File",
|
||||
"options": "JSON",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "attachment_link",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Attachment Link",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-12 15:22:35.283678",
|
||||
"links": [],
|
||||
"modified": "2020-05-31 22:31:12.886950",
|
||||
"modified_by": "umair@erpnext.com",
|
||||
"module": "Desk",
|
||||
"name": "Notification Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ def enqueue_create_notification(users, doc):
|
|||
|
||||
if isinstance(users, frappe.string_types):
|
||||
users = [user.strip() for user in users.split(',') if user.strip()]
|
||||
users = list(set(users))
|
||||
|
||||
frappe.enqueue(
|
||||
'frappe.desk.doctype.notification_log.notification_log.make_notification_logs',
|
||||
|
|
@ -58,6 +59,7 @@ def enqueue_create_notification(users, doc):
|
|||
|
||||
def make_notification_logs(doc, users):
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
|
||||
for user in users:
|
||||
if frappe.db.exists('User', user):
|
||||
if is_notifications_enabled(user):
|
||||
|
|
@ -68,7 +70,7 @@ def make_notification_logs(doc, users):
|
|||
_doc.update(doc)
|
||||
_doc.for_user = user
|
||||
_doc.subject = _doc.subject.replace('<div>', '').replace('</div>', '')
|
||||
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point':
|
||||
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
|
||||
_doc.insert(ignore_permissions=True)
|
||||
|
||||
def send_notification_email(doc):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-09-11 22:15:44.851526",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -21,52 +22,68 @@
|
|||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
"label": "Enabled",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscribed_documents",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Subscribed Documents",
|
||||
"options": "Notification Subscribed Document"
|
||||
"options": "Notification Subscribed Document",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email Settings"
|
||||
"label": "Email Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enable_email_notifications",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Email Notifications"
|
||||
"label": "Enable Email Notifications",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_mention",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mentions"
|
||||
"label": "Mentions",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_assignment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Assignments"
|
||||
"label": "Assignments",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_energy_point",
|
||||
"fieldtype": "Check",
|
||||
"label": "Energy Points"
|
||||
"label": "Energy Points",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_email_notifications",
|
||||
"fieldname": "enable_email_share",
|
||||
"fieldtype": "Check",
|
||||
"label": "Document Share"
|
||||
"label": "Document Share",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "__user",
|
||||
|
|
@ -75,18 +92,23 @@
|
|||
"hidden": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Seen"
|
||||
"label": "Seen",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-19 12:57:59.356786",
|
||||
"links": [],
|
||||
"modified": "2020-05-31 22:16:40.798019",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Notification Settings",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ def is_email_notifications_enabled_for_type(user, notification_type):
|
|||
if not is_email_notifications_enabled(user):
|
||||
return False
|
||||
|
||||
if notification_type == 'Alert':
|
||||
return False
|
||||
|
||||
fieldname = 'enable_email_' + frappe.scrub(notification_type)
|
||||
enabled = frappe.db.get_value('Notification Settings', user, fieldname)
|
||||
if enabled is None:
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ToDo(Document):
|
|||
filters={
|
||||
"reference_type": self.reference_type,
|
||||
"reference_name": self.reference_name,
|
||||
"status": "Open"
|
||||
"status": ("!=", "Cancelled")
|
||||
},
|
||||
fields=["owner"], as_list=True)]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@ import frappe
|
|||
@frappe.whitelist()
|
||||
def get_list_settings(doctype):
|
||||
try:
|
||||
return frappe.get_cached_doc("List View Setting", doctype)
|
||||
return frappe.get_cached_doc("List View Settings", doctype)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_messages()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_list_settings(doctype, values):
|
||||
try:
|
||||
doc = frappe.get_doc("List View Setting", doctype)
|
||||
doc = frappe.get_doc("List View Settings", doctype)
|
||||
except frappe.DoesNotExistError:
|
||||
doc = frappe.new_doc("List View Setting")
|
||||
doc = frappe.new_doc("List View Settings")
|
||||
doc.name = doctype
|
||||
frappe.clear_messages()
|
||||
doc.update(frappe.parse_json(values))
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ frappe.ui.form.on("Notification", {
|
|||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frm.toggle_reqd("recipients", frm.doc.channel=="Email");
|
||||
frappe.notification.setup_fieldname_select(frm);
|
||||
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
|
||||
frm.trigger('event');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2014-07-11 17:18:09.923399",
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"days_in_advance",
|
||||
"value_changed",
|
||||
"sender",
|
||||
"send_system_notification",
|
||||
"sender_email",
|
||||
"section_break_9",
|
||||
"condition",
|
||||
|
|
@ -46,32 +48,43 @@
|
|||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
"label": "Enabled",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Email",
|
||||
"depends_on": "eval: !doc.disable_channel",
|
||||
"fieldname": "channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Channel",
|
||||
"options": "Email\nSlack",
|
||||
"options": "Email\nSlack\nSystem Notification",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Slack'",
|
||||
"fieldname": "slack_webhook_url",
|
||||
"fieldtype": "Link",
|
||||
"label": "Slack Channel",
|
||||
"options": "Slack Webhook URL"
|
||||
"mandatory_depends_on": "eval:doc.channel=='Slack'",
|
||||
"options": "Slack Webhook URL",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "filters",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters"
|
||||
"label": "Filters",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>{{ doc.name }} Delivered</code></pre></div>",
|
||||
|
|
@ -80,7 +93,9 @@
|
|||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
|
|
@ -90,13 +105,17 @@
|
|||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
"label": "Is Standard",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_standard",
|
||||
|
|
@ -104,11 +123,15 @@
|
|||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
"options": "Module Def",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "event",
|
||||
|
|
@ -117,21 +140,27 @@
|
|||
"label": "Send Alert On",
|
||||
"options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event=='Method'",
|
||||
"description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)",
|
||||
"fieldname": "method",
|
||||
"fieldtype": "Data",
|
||||
"label": "Trigger Method"
|
||||
"label": "Trigger Method",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"",
|
||||
"description": "Send alert if date matches this field's value",
|
||||
"fieldname": "date_changed",
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Date"
|
||||
"label": "Reference Date",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -139,31 +168,41 @@
|
|||
"description": "Send days before or after the reference date",
|
||||
"fieldname": "days_in_advance",
|
||||
"fieldtype": "Int",
|
||||
"label": "Days Before or After"
|
||||
"label": "Days Before or After",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event==\"Value Change\"",
|
||||
"description": "Send alert if this field's value changes",
|
||||
"fieldname": "value_changed",
|
||||
"fieldtype": "Select",
|
||||
"label": "Value Changed"
|
||||
"label": "Value Changed",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sender",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sender",
|
||||
"options": "Email Account"
|
||||
"options": "Email Account",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sender_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "Optional: The alert will be sent if this expression is true",
|
||||
|
|
@ -171,99 +210,143 @@
|
|||
"fieldtype": "Code",
|
||||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Condition"
|
||||
"label": "Condition",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "html_7",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>\n"
|
||||
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>\n",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "property_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Set Property After Alert"
|
||||
"label": "Set Property After Alert",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_property_after_alert",
|
||||
"fieldtype": "Select",
|
||||
"label": "Set Property After Alert"
|
||||
"label": "Set Property After Alert",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "property_value",
|
||||
"fieldtype": "Data",
|
||||
"label": "Value To Be Set"
|
||||
"label": "Value To Be Set",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Email'",
|
||||
"depends_on": "eval:doc.channel!=='Slack'",
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Recipients"
|
||||
"label": "Recipients",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "recipients",
|
||||
"fieldtype": "Table",
|
||||
"label": "Recipients",
|
||||
"options": "Notification Recipient"
|
||||
"mandatory_depends_on": "eval:doc.channel!=='Slack'",
|
||||
"options": "Notification Recipient",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "message_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Message"
|
||||
"label": "Message",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Add your message here",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Code",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Message"
|
||||
"label": "Message",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Email'",
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
"options": "<h5>Message Example</h5>\n\n<pre><h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n</pre>"
|
||||
"options": "<h5>Message Example</h5>\n\n<pre><h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n</pre>",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.channel=='Slack'",
|
||||
"fieldname": "slack_message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
"options": "<h5>Message Example</h5>\n\n<pre>*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n</pre>"
|
||||
"options": "<h5>Message Example</h5>\n\n<pre>*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n</pre>",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "view_properties",
|
||||
"fieldtype": "Button",
|
||||
"label": "View Properties (via Customize Form)"
|
||||
"label": "View Properties (via Customize Form)",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "attach_print",
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
"label": "Print Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "attach_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Attach Print"
|
||||
"label": "Attach Print",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "attach_print",
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format",
|
||||
"options": "Print Format"
|
||||
"options": "Print Format",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.channel !== 'System Notification'",
|
||||
"description": "If enabled, the notification will show up in the notifications dropdown on the top right corner of the navigation bar.",
|
||||
"fieldname": "send_system_notification",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send System Notification",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-envelope",
|
||||
"modified": "2019-07-15 13:17:02.585013",
|
||||
"links": [],
|
||||
"modified": "2020-05-29 16:03:10.914526",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Notification",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from frappe.utils.jinja import validate_template
|
|||
from frappe.modules.utils import export_module_json, get_doc_module
|
||||
from six import string_types
|
||||
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
|
||||
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification
|
||||
|
||||
class Notification(Document):
|
||||
def onload(self):
|
||||
|
|
@ -125,6 +126,9 @@ def get_context(context):
|
|||
if self.channel == 'Slack':
|
||||
self.send_a_slack_msg(doc, context)
|
||||
|
||||
if self.channel == 'System Notification' or self.send_system_notification:
|
||||
self.create_system_notification(doc, context)
|
||||
|
||||
if self.set_property_after_alert:
|
||||
allow_update = True
|
||||
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
|
||||
|
|
@ -143,6 +147,25 @@ def get_context(context):
|
|||
except Exception:
|
||||
frappe.log_error(title='Document update failed', message=frappe.get_traceback())
|
||||
|
||||
def create_system_notification(self, doc, context):
|
||||
subject = self.subject
|
||||
if "{" in subject:
|
||||
subject = frappe.render_template(self.subject, context)
|
||||
|
||||
attachments = self.get_attachment(doc)
|
||||
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
|
||||
users = recipients + cc + bcc
|
||||
|
||||
notification_doc = {
|
||||
'type': 'Alert',
|
||||
'document_type': doc.doctype,
|
||||
'document_name': doc.name,
|
||||
'subject': subject,
|
||||
'email_content': frappe.render_template(self.message, context),
|
||||
'attached_file': attachments and json.dumps(attachments[0])
|
||||
}
|
||||
enqueue_create_notification(users, notification_doc)
|
||||
|
||||
def send_an_email(self, doc, context):
|
||||
from email.utils import formataddr
|
||||
subject = self.subject
|
||||
|
|
@ -228,8 +251,7 @@ def get_context(context):
|
|||
|
||||
# ignoring attachment as draft and cancelled documents are not allowed to print
|
||||
status = "Draft" if doc.docstatus == 0 else "Cancelled"
|
||||
frappe.throw(_("""Not allowed to attach {0} document,
|
||||
please enable Allow Print For {0} in Print Settings""".format(status)),
|
||||
frappe.throw(_("""Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings""").format(status),
|
||||
title=_("Error in Notification"))
|
||||
else:
|
||||
return [{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ if sys.version_info.major == 2:
|
|||
else:
|
||||
from builtins import FileNotFoundError
|
||||
|
||||
class SiteNotSpecifiedError(Exception):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = "Please specify --site sitename"
|
||||
super(Exception, self).__init__(self.message)
|
||||
|
||||
class ValidationError(Exception):
|
||||
http_status_code = 417
|
||||
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ def make_site_dirs():
|
|||
os.path.join(site_private_path, 'backups'),
|
||||
os.path.join(site_public_path, 'files'),
|
||||
os.path.join(site_private_path, 'files'),
|
||||
os.path.join(frappe.local.site_path, 'logs'),
|
||||
os.path.join(frappe.local.site_path, 'task-logs')):
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
|
@ -298,7 +299,8 @@ def remove_missing_apps():
|
|||
|
||||
def extract_sql_gzip(sql_gz_path):
|
||||
try:
|
||||
subprocess.check_call(['gzip', '-d', '-v', '-f', sql_gz_path])
|
||||
# kdvf - keep, decompress, verbose, force
|
||||
subprocess.check_call(['gzip', '-kdvf', sql_gz_path])
|
||||
except:
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_url, call_hook_method, cint, get_timestamp
|
||||
|
|
@ -317,6 +319,20 @@ class RazorpaySettings(Document):
|
|||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
||||
def verify_signature(self, body, signature, key):
|
||||
key = bytes(key, 'utf-8')
|
||||
body = bytes(body, 'utf-8')
|
||||
|
||||
dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256)
|
||||
|
||||
generated_signature = dig.hexdigest()
|
||||
result = hmac.compare_digest(generated_signature, signature)
|
||||
|
||||
if not result:
|
||||
frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError)
|
||||
|
||||
return result
|
||||
|
||||
def capture_payment(is_sandbox=False, sanbox_response=None):
|
||||
"""
|
||||
Verifies the purchase as complete by the merchant.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
|
|||
|
||||
def migrate_to(local_site, frappe_provider):
|
||||
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
|
||||
frappe_provider = "frappecloud.com"
|
||||
return frappecloud_migrator(local_site, frappe_provider)
|
||||
else:
|
||||
print("{} is not supported yet".format(frappe_provider))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,129 @@ import requests
|
|||
import frappe
|
||||
import frappe.utils.backups
|
||||
from frappe.utils import get_installed_apps_info
|
||||
from frappe.utils.commands import render_table, add_line_after
|
||||
from frappe.utils.commands import render_table, add_line_after, add_line_before
|
||||
|
||||
|
||||
# TODO: check upgrade compatibility
|
||||
|
||||
|
||||
def render_actions_table():
|
||||
actions_table = [["#", "Action"]]
|
||||
actions = []
|
||||
|
||||
for n, action in enumerate(migrator_actions):
|
||||
actions_table.append([n+1, action["title"]])
|
||||
actions.append(action["fn"])
|
||||
|
||||
render_table(actions_table)
|
||||
return actions
|
||||
|
||||
|
||||
def render_site_table(sites_info):
|
||||
sites_table = [["#", "Site Name", "Status"]]
|
||||
available_sites = []
|
||||
|
||||
for n, site_data in enumerate(sites_info):
|
||||
name, status = site_data["name"], site_data["status"]
|
||||
if status in ("Active", "Broken"):
|
||||
sites_table.append([n + 1, name, status])
|
||||
available_sites.append(name)
|
||||
|
||||
render_table(sites_table)
|
||||
return available_sites
|
||||
|
||||
|
||||
def render_teams_table(teams):
|
||||
teams_table = [["#", "Team"]]
|
||||
|
||||
for n, team in enumerate(teams):
|
||||
teams_table.append([n+1, team])
|
||||
|
||||
render_table(teams_table)
|
||||
|
||||
|
||||
def render_plan_table(plans_list):
|
||||
plans_table = [["Plan", "CPU Time"]]
|
||||
visible_headers = ["name", "cpu_time_per_day"]
|
||||
|
||||
for plan in plans_list:
|
||||
plan, cpu_time = [plan[header] for header in visible_headers]
|
||||
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
|
||||
|
||||
render_table(plans_table)
|
||||
|
||||
|
||||
def render_group_table(app_groups):
|
||||
# title row
|
||||
app_groups_table = [["#", "App Group", "Apps"]]
|
||||
|
||||
# all rows
|
||||
for idx, app_group in enumerate(app_groups):
|
||||
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
|
||||
row = [idx + 1, app_group["name"], apps_list]
|
||||
app_groups_table.append(row)
|
||||
|
||||
render_table(app_groups_table)
|
||||
|
||||
|
||||
def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
|
||||
message = message or "Request failed with error code {}".format(request.status_code)
|
||||
response = html2text(request.text) if traceback else ""
|
||||
|
||||
print("{0}{1}".format(message, "\n" + response))
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def select_primary_action():
|
||||
actions = render_actions_table()
|
||||
idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
|
||||
|
||||
return actions[idx]
|
||||
|
||||
|
||||
@add_line_after
|
||||
def select_site():
|
||||
get_all_sites_request = session.post(all_site_url, headers={
|
||||
"accept": "application/json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
})
|
||||
|
||||
if get_all_sites_request.ok:
|
||||
all_sites = get_all_sites_request.json()["message"]
|
||||
available_sites = render_site_table(all_sites)
|
||||
|
||||
while True:
|
||||
selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
|
||||
if selected_site in available_sites:
|
||||
return selected_site
|
||||
else:
|
||||
print("Site {} does not exist. Try again ❌".format(selected_site))
|
||||
else:
|
||||
print("Couldn't retrive sites list...Try again later")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@add_line_before
|
||||
def select_team(session):
|
||||
# get team options
|
||||
account_details_sc = session.post(account_details_url)
|
||||
if account_details_sc.ok:
|
||||
account_details = account_details_sc.json()["message"]
|
||||
available_teams = account_details["teams"]
|
||||
|
||||
# ask if they want to select, go ahead with if only one exists
|
||||
if len(available_teams) == 1:
|
||||
team = available_teams[0]
|
||||
else:
|
||||
render_teams_table(available_teams)
|
||||
idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
|
||||
team = available_teams[idx]
|
||||
|
||||
print("Team '{}' set for current session".format(team))
|
||||
|
||||
return team
|
||||
|
||||
|
||||
def get_new_site_options():
|
||||
|
|
@ -46,21 +168,6 @@ def is_subdomain_available(subdomain):
|
|||
return available
|
||||
|
||||
|
||||
def render_plan_table(plans_list):
|
||||
plans_table = []
|
||||
|
||||
# title row
|
||||
visible_headers = ["name", "cpu_time_per_day"]
|
||||
plans_table.append(["Plan", "CPU Time"])
|
||||
|
||||
# all rows
|
||||
for plan in plans_list:
|
||||
plan, cpu_time = [plan[header] for header in visible_headers]
|
||||
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
|
||||
|
||||
render_table(plans_table)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def choose_plan(plans_list):
|
||||
print("{} plans available".format(len(plans_list)))
|
||||
|
|
@ -113,19 +220,6 @@ def check_app_compat(available_group):
|
|||
return is_compat, filtered_apps
|
||||
|
||||
|
||||
def render_group_table(app_groups):
|
||||
# title row
|
||||
app_groups_table = [["#", "App Group", "Apps"]]
|
||||
|
||||
# all rows
|
||||
for idx, app_group in enumerate(app_groups):
|
||||
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
|
||||
row = [idx + 1, app_group["name"], apps_list]
|
||||
app_groups_table.append(row)
|
||||
|
||||
render_table(app_groups_table)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def filter_apps(app_groups):
|
||||
render_group_table(app_groups)
|
||||
|
|
@ -148,24 +242,6 @@ def filter_apps(app_groups):
|
|||
|
||||
return selected_group["name"], filtered_apps
|
||||
|
||||
@add_line_after
|
||||
def create_session():
|
||||
# take user input from STDIN
|
||||
username = click.prompt("Username").strip()
|
||||
password = getpass.unix_getpass()
|
||||
|
||||
auth_credentials = {"usr": username, "pwd": password}
|
||||
|
||||
session = requests.Session()
|
||||
login_sc = session.post(login_url, auth_credentials)
|
||||
|
||||
if login_sc.ok:
|
||||
print("Authorization Successful! ✅")
|
||||
session.headers.update({"X-Press-Team": username})
|
||||
return session
|
||||
else:
|
||||
print("Authorization Failed with Error Code {}".format(login_sc.status_code))
|
||||
|
||||
|
||||
@add_line_after
|
||||
def get_subdomain(domain):
|
||||
|
|
@ -208,61 +284,114 @@ def upload_backup(local_site):
|
|||
return files_uploaded
|
||||
|
||||
|
||||
def frappecloud_migrator(local_site, remote_site):
|
||||
global login_url, upload_url, files_url, options_url, site_exists_url, session
|
||||
def new_site(local_site):
|
||||
# get new site options
|
||||
site_options = get_new_site_options()
|
||||
|
||||
# set preferences from site options
|
||||
subdomain = get_subdomain(site_options["domain"])
|
||||
plan = choose_plan(site_options["plans"])
|
||||
|
||||
app_groups = site_options["groups"]
|
||||
selected_group, filtered_apps = filter_apps(app_groups)
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"site": {
|
||||
"apps": filtered_apps,
|
||||
"files": files_uploaded,
|
||||
"group": selected_group,
|
||||
"name": subdomain,
|
||||
"plan": plan
|
||||
}
|
||||
})
|
||||
|
||||
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
|
||||
site_creation_request = session.post(upload_url, payload)
|
||||
|
||||
if site_creation_request.ok:
|
||||
site_url = site_creation_request.json()["message"]
|
||||
print("Your site {} is being migrated ✨".format(local_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
|
||||
print("Your site URL: {}".format(site_url))
|
||||
else:
|
||||
handle_request_failure(site_creation_request)
|
||||
|
||||
|
||||
def restore_site(local_site):
|
||||
# get list of existing sites they can restore
|
||||
selected_site = select_site()
|
||||
|
||||
# TODO: check if they can restore it
|
||||
|
||||
click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
|
||||
|
||||
# backup site
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"name": selected_site,
|
||||
"files": files_uploaded
|
||||
})
|
||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||
site_restore_request = session.post(restore_site_url, payload, headers=headers)
|
||||
|
||||
if site_restore_request.ok:
|
||||
print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
|
||||
print("Your site URL: {}".format(selected_site))
|
||||
else:
|
||||
handle_request_failure(site_restore_request)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def create_session():
|
||||
print("Frappe Cloud credentials @ {}".format(remote_site))
|
||||
|
||||
# take user input from STDIN
|
||||
username = click.prompt("Username").strip()
|
||||
password = getpass.unix_getpass()
|
||||
|
||||
auth_credentials = {"usr": username, "pwd": password}
|
||||
|
||||
session = requests.Session()
|
||||
login_sc = session.post(login_url, auth_credentials)
|
||||
|
||||
if login_sc.ok:
|
||||
print("Authorization Successful! ✅")
|
||||
team = select_team(session)
|
||||
session.headers.update({"X-Press-Team": team })
|
||||
return session
|
||||
else:
|
||||
handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
|
||||
|
||||
|
||||
def frappecloud_migrator(local_site, frappecloud_site):
|
||||
global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
|
||||
global session, migrator_actions, remote_site
|
||||
|
||||
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
|
||||
|
||||
login_url = "https://{}/api/method/login".format(remote_site)
|
||||
upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
|
||||
files_url = "https://{}/api/method/upload_file".format(remote_site)
|
||||
options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
|
||||
site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
|
||||
account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
|
||||
all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
|
||||
restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
|
||||
|
||||
print("Frappe Cloud credentials @ {}".format(remote_site))
|
||||
migrator_actions = [
|
||||
{ "title": "Create a new site", "fn": new_site },
|
||||
{ "title": "Restore to an existing site", "fn": restore_site }
|
||||
]
|
||||
|
||||
# get credentials + auth user + start session
|
||||
session = create_session()
|
||||
|
||||
if session:
|
||||
# connect to site db
|
||||
frappe.init(site=local_site)
|
||||
frappe.connect()
|
||||
# available actions defined in migrator_actions
|
||||
primary_action = select_primary_action()
|
||||
|
||||
# get new site options
|
||||
site_options = get_new_site_options()
|
||||
|
||||
# set preferences from site options
|
||||
subdomain = get_subdomain(site_options["domain"])
|
||||
plan = choose_plan(site_options["plans"])
|
||||
|
||||
app_groups = site_options["groups"]
|
||||
selected_group, filtered_apps = filter_apps(app_groups)
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"site": {
|
||||
"apps": filtered_apps,
|
||||
"files": files_uploaded,
|
||||
"group": selected_group,
|
||||
"name": subdomain,
|
||||
"plan": plan
|
||||
}
|
||||
})
|
||||
|
||||
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
|
||||
site_creation_request = session.post(upload_url, payload)
|
||||
frappe.destroy()
|
||||
|
||||
if site_creation_request.ok:
|
||||
site_url = site_creation_request.json()["message"]
|
||||
print("Your site {} is being migrated ✨".format(local_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
|
||||
print("Your site URL: {}".format(site_url))
|
||||
else:
|
||||
print("Request failed with error code {}".format(site_creation_request.status_code))
|
||||
reason = html2text(site_creation_request.text)
|
||||
print(reason)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
sys.exit(1)
|
||||
primary_action(local_site)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import frappe, json, copy, re
|
|||
from frappe.model import optional_fields
|
||||
from frappe.client import check_parent_permission
|
||||
from frappe.model.utils.user_settings import get_user_settings, update_user_settings
|
||||
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, nowdate
|
||||
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range
|
||||
from frappe.model.meta import get_table_columns
|
||||
|
||||
class DatabaseQuery(object):
|
||||
|
|
@ -354,7 +354,9 @@ class DatabaseQuery(object):
|
|||
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
|
||||
"""
|
||||
|
||||
f = get_filter(self.doctype, f)
|
||||
from frappe.boot import get_additional_filters_from_hooks
|
||||
additional_filters_config = get_additional_filters_from_hooks()
|
||||
f = get_filter(self.doctype, f, additional_filters_config)
|
||||
|
||||
tname = ('`tab' + f.doctype + '`')
|
||||
if not tname in self.tables:
|
||||
|
|
@ -368,6 +370,9 @@ class DatabaseQuery(object):
|
|||
|
||||
can_be_null = True
|
||||
|
||||
if f.operator.lower() in additional_filters_config:
|
||||
f.update(get_additional_filter_field(additional_filters_config, f, f.value))
|
||||
|
||||
# prepare in condition
|
||||
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'):
|
||||
values = f.value or ''
|
||||
|
|
@ -426,29 +431,8 @@ class DatabaseQuery(object):
|
|||
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
|
||||
can_be_null = False
|
||||
|
||||
if f.operator.lower() in ('previous', 'next'):
|
||||
if f.operator.lower() == "previous":
|
||||
if f.value == "1 week":
|
||||
date_range = [add_to_date(nowdate(), days=-7), nowdate()]
|
||||
elif f.value == "1 month":
|
||||
date_range = [add_to_date(nowdate(), months=-1), nowdate()]
|
||||
elif f.value == "3 months":
|
||||
date_range = [add_to_date(nowdate(), months=-3), nowdate()]
|
||||
elif f.value == "6 months":
|
||||
date_range = [add_to_date(nowdate(), months=-6), nowdate()]
|
||||
elif f.value == "1 year":
|
||||
date_range = [add_to_date(nowdate(), years=-1), nowdate()]
|
||||
elif f.operator.lower() == "next":
|
||||
if f.value == "1 week":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), days=7)]
|
||||
elif f.value == "1 month":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), months=1)]
|
||||
elif f.value == "3 months":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), months=3)]
|
||||
elif f.value == "6 months":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), months=6)]
|
||||
elif f.value == "1 year":
|
||||
date_range = [nowdate(), add_to_date(nowdate(), years=1)]
|
||||
if f.operator.lower() in ('previous', 'next', 'timespan'):
|
||||
date_range = get_date_range(f.operator.lower(), f.value)
|
||||
f.operator = "Between"
|
||||
f.value = date_range
|
||||
fallback = "'0001-01-01 00:00:00'"
|
||||
|
|
@ -843,4 +827,31 @@ def get_between_date_filter(value, df=None):
|
|||
frappe.db.format_date(from_date),
|
||||
frappe.db.format_date(to_date))
|
||||
|
||||
return data
|
||||
return data
|
||||
|
||||
def get_additional_filter_field(additional_filters_config, f, value):
|
||||
additional_filter = additional_filters_config[f.operator.lower()]
|
||||
f = frappe._dict(frappe.get_attr(additional_filter['get_field'])())
|
||||
if f.query_value:
|
||||
for option in f.options:
|
||||
option = frappe._dict(option)
|
||||
if option.value == value:
|
||||
f.value = option.query_value
|
||||
return f
|
||||
|
||||
def get_date_range(operator, value):
|
||||
timespan_map = {
|
||||
'1 week': 'week',
|
||||
'1 month': 'month',
|
||||
'3 months': 'quarter',
|
||||
'6 months': '6 months',
|
||||
'1 year': 'year',
|
||||
}
|
||||
period_map = {
|
||||
'previous': 'last',
|
||||
'next': 'next',
|
||||
}
|
||||
|
||||
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
|
||||
|
||||
return get_timespan_date_range(timespan)
|
||||
|
|
@ -56,6 +56,8 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
|
|||
|
||||
if not merge:
|
||||
rename_parent_and_child(doctype, old, new, meta)
|
||||
else:
|
||||
update_assignments(old, new, doctype)
|
||||
|
||||
# update link fields' values
|
||||
link_fields = get_link_fields(doctype)
|
||||
|
|
@ -104,6 +106,27 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
|
|||
|
||||
return new
|
||||
|
||||
def update_assignments(old, new, doctype):
|
||||
old_assignments = frappe.parse_json(frappe.db.get_value(doctype, old, '_assign')) or []
|
||||
new_assignments = frappe.parse_json(frappe.db.get_value(doctype, new, '_assign')) or []
|
||||
common_assignments = list(set(old_assignments).intersection(new_assignments))
|
||||
|
||||
for user in common_assignments:
|
||||
# delete todos linked to old doc
|
||||
todos = frappe.db.get_all('ToDo',
|
||||
{
|
||||
'owner': user,
|
||||
'reference_type': doctype,
|
||||
'reference_name': old,
|
||||
},
|
||||
['name', 'description']
|
||||
)
|
||||
|
||||
for todo in todos:
|
||||
frappe.delete_doc('ToDo', todo.name)
|
||||
|
||||
unique_assignments = list(set(old_assignments + new_assignments))
|
||||
frappe.db.set_value(doctype, new, '_assign', frappe.as_json(unique_assignments, indent=0))
|
||||
|
||||
def update_user_settings(old, new, link_fields):
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ def set_workflow_state_on_action(doc, workflow_name, action):
|
|||
return
|
||||
|
||||
action_map = {
|
||||
'update_after_submit': '1',
|
||||
'submit': '1',
|
||||
'cancel': '2'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
|
|||
frappe.patches.v13_0.migrate_translation_column_data
|
||||
frappe.patches.v13_0.set_read_times
|
||||
frappe.patches.v13_0.remove_web_view
|
||||
frappe.patches.v13_0.site_wise_logging
|
||||
frappe.patches.v13_0.set_unique_for_page_view
|
||||
frappe.patches.v13_0.remove_tailwind_from_page_builder
|
||||
frappe.patches.v13_0.rename_onboarding
|
||||
|
|
@ -285,4 +286,5 @@ frappe.patches.v13_0.email_unsubscribe
|
|||
execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1)
|
||||
execute:frappe.delete_doc("DocType", "Onboarding Slide")
|
||||
execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
|
||||
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
|
||||
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
|
||||
frappe.patches.v13_0.update_date_filters_in_user_settings
|
||||
|
|
|
|||
10
frappe/patches/v13_0/site_wise_logging.py
Normal file
10
frappe/patches/v13_0/site_wise_logging.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import os
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
site = frappe.local.site
|
||||
|
||||
log_folder = os.path.join(site, 'logs')
|
||||
if not os.path.exists(log_folder):
|
||||
os.mkdir(log_folder)
|
||||
54
frappe/patches/v13_0/update_date_filters_in_user_settings.py
Normal file
54
frappe/patches/v13_0/update_date_filters_in_user_settings.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe, json
|
||||
from frappe.model.utils.user_settings import update_user_settings, sync_user_settings
|
||||
|
||||
def execute():
|
||||
users = frappe.db.sql("select distinct(user) from `__UserSettings`", as_dict=True)
|
||||
|
||||
for user in users:
|
||||
user_settings = frappe.db.sql('''
|
||||
select
|
||||
* from `__UserSettings`
|
||||
where
|
||||
user="{user}"
|
||||
'''.format(user = user.user), as_dict=True)
|
||||
|
||||
for setting in user_settings:
|
||||
data = frappe.parse_json(setting.get('data'))
|
||||
if data:
|
||||
for key in data:
|
||||
update_user_setting_filters(data, key, setting)
|
||||
|
||||
sync_user_settings()
|
||||
|
||||
|
||||
def update_user_setting_filters(data, key, user_setting):
|
||||
timespan_map = {
|
||||
'1 week': 'week',
|
||||
'1 month': 'month',
|
||||
'3 months': 'quarter',
|
||||
'6 months': '6 months',
|
||||
'1 year': 'year',
|
||||
}
|
||||
|
||||
period_map = {
|
||||
'Previous': 'last',
|
||||
'Next': 'next'
|
||||
}
|
||||
|
||||
if data.get(key):
|
||||
update = False
|
||||
if isinstance(data.get(key), dict):
|
||||
filters = data.get(key).get('filters')
|
||||
if filters and isinstance(filters, list):
|
||||
for f in filters:
|
||||
if f[2] == 'Next' or f[2] == 'Previous':
|
||||
update = True
|
||||
f[3] = period_map[f[2]] + ' ' + timespan_map[f[3]]
|
||||
f[2] = 'Timespan'
|
||||
|
||||
if update:
|
||||
data[key]['filters'] = filters
|
||||
update_user_settings(user_setting['doctype'], json.dumps(data), for_update=True)
|
||||
|
||||
|
||||
|
|
@ -338,6 +338,11 @@ frappe.views.BaseList = class BaseList {
|
|||
: [];
|
||||
}
|
||||
|
||||
get_filter_value(fieldname) {
|
||||
return this.get_filters_for_args().filter(f => f[1] == fieldname)[0] &&
|
||||
this.get_filters_for_args().filter(f => f[1] == fieldname)[0][3];
|
||||
}
|
||||
|
||||
get_args() {
|
||||
return {
|
||||
doctype: this.doctype,
|
||||
|
|
|
|||
383
frappe/public/js/frappe/list/list_settings.js
Normal file
383
frappe/public/js/frappe/list/list_settings.js
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
export default class ListSettings {
|
||||
constructor({ listview, doctype, meta, settings }) {
|
||||
if (!doctype) {
|
||||
frappe.throw(__('Doctype required'));
|
||||
}
|
||||
|
||||
this.listview = listview;
|
||||
this.doctype = doctype;
|
||||
this.meta = meta;
|
||||
this.settings = settings;
|
||||
this.dialog = null;
|
||||
this.fields = this.settings && this.settings.fields ? JSON.parse(this.settings.fields) : [];
|
||||
this.subject_field = null;
|
||||
|
||||
frappe.model.with_doctype("List View Settings", () => {
|
||||
this.make();
|
||||
this.get_listview_fields(meta);
|
||||
this.setup_fields();
|
||||
this.setup_remove_fields();
|
||||
this.add_new_fields();
|
||||
this.show_dialog();
|
||||
});
|
||||
}
|
||||
|
||||
make() {
|
||||
let me = this;
|
||||
|
||||
let list_view_settings = frappe.get_meta("List View Settings");
|
||||
|
||||
me.dialog = new frappe.ui.Dialog({
|
||||
title: __("{0} Settings", [__(me.doctype)]),
|
||||
fields: list_view_settings.fields
|
||||
});
|
||||
me.dialog.set_values(me.settings);
|
||||
me.dialog.set_primary_action(__('Save'), () => {
|
||||
let values = me.dialog.get_values();
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Saving"),
|
||||
indicator: "green"
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.list_view_settings.list_view_settings.save_listview_settings",
|
||||
args: {
|
||||
doctype: me.doctype,
|
||||
listview_settings: values,
|
||||
removed_listview_fields: me.removed_fields || []
|
||||
},
|
||||
callback: function (r) {
|
||||
me.listview.refresh_columns(r.message.meta, r.message.listview_settings);
|
||||
me.dialog.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
me.dialog.fields_dict["total_fields"].df.onchange = () => me.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
let me = this;
|
||||
|
||||
me.setup_fields();
|
||||
me.add_new_fields();
|
||||
me.setup_remove_fields();
|
||||
}
|
||||
|
||||
show_dialog() {
|
||||
let me = this;
|
||||
|
||||
if (!this.settings.fields) {
|
||||
me.update_fields();
|
||||
}
|
||||
|
||||
if (!me.dialog.get_value("total_fields")) {
|
||||
let field_count = me.fields.length;
|
||||
|
||||
if (field_count < 4) {
|
||||
field_count = 4;
|
||||
} else if (field_count > 10) {
|
||||
field_count = 4;
|
||||
}
|
||||
|
||||
me.dialog.set_value("total_fields", field_count);
|
||||
}
|
||||
|
||||
me.dialog.show();
|
||||
}
|
||||
|
||||
setup_fields() {
|
||||
function is_status_field(field) {
|
||||
return field.fieldname === "status_field";
|
||||
}
|
||||
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let wrapper = fields_html.$wrapper[0];
|
||||
let fields = ``;
|
||||
let total_fields = me.dialog.get_values().total_fields || me.settings.total_fields;
|
||||
|
||||
for (let idx in me.fields) {
|
||||
if (idx == parseInt(total_fields)) {
|
||||
break;
|
||||
}
|
||||
let is_sortable = (idx == 0) ? `` : `sortable`;
|
||||
let show_sortable_handle = (idx == 0) ? `hide` : ``;
|
||||
let can_remove = (idx == 0 || is_status_field(me.fields[idx])) ? `hide` : ``;
|
||||
|
||||
fields += `
|
||||
<div class="control-input flex align-center form-control fields_order ${is_sortable}"
|
||||
style="display: block; margin-bottom: 5px;" data-fieldname="${me.fields[idx].fieldname}"
|
||||
data-label="${me.fields[idx].label}" data-type="${me.fields[idx].type}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-1">
|
||||
<i class="fa fa-bars text-muted sortable-handle ${show_sortable_handle}" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-md-10" style="padding-left:0px;">
|
||||
${me.fields[idx].label}
|
||||
</div>
|
||||
<div class="col-md-1 ${can_remove}">
|
||||
<a class="text-muted remove-field" data-fieldname="${me.fields[idx].fieldname}">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
fields_html.html(`
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label" style="padding-right: 0px;">Fields</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
${fields}
|
||||
</div>
|
||||
<p class="help-box small text-muted hidden-xs">
|
||||
<a class="add-new-fields text-muted">
|
||||
+ Add / Remove Fields
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
new Sortable(wrapper.getElementsByClassName("control-input-wrapper")[0], {
|
||||
handle: '.sortable-handle',
|
||||
draggable: '.sortable',
|
||||
onUpdate: () => {
|
||||
me.update_fields();
|
||||
me.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_new_fields() {
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let add_new_fields = fields_html.$wrapper[0].getElementsByClassName("add-new-fields")[0];
|
||||
add_new_fields.onclick = () => me.column_selector();
|
||||
}
|
||||
|
||||
setup_remove_fields() {
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let remove_fields = fields_html.$wrapper[0].getElementsByClassName("remove-field");
|
||||
|
||||
for (let idx = 0; idx < remove_fields.length; idx++) {
|
||||
remove_fields.item(idx).onclick = () => me.remove_fields(remove_fields.item(idx).getAttribute("data-fieldname"));
|
||||
}
|
||||
}
|
||||
|
||||
remove_fields(fieldname) {
|
||||
let me = this;
|
||||
let existing_fields = me.fields.map(f => f.fieldname);
|
||||
|
||||
for (let idx in me.fields) {
|
||||
let field = me.fields[idx];
|
||||
|
||||
if (field.fieldname == fieldname) {
|
||||
me.fields.splice(idx, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
me.set_removed_fields(me.get_removed_listview_fields(me.fields.map(f => f.fieldname), existing_fields));
|
||||
me.refresh();
|
||||
me.update_fields();
|
||||
}
|
||||
|
||||
update_fields() {
|
||||
let me = this;
|
||||
|
||||
let fields_html = me.dialog.get_field("fields_html");
|
||||
let wrapper = fields_html.$wrapper[0];
|
||||
|
||||
let fields_order = wrapper.getElementsByClassName("fields_order");
|
||||
me.fields = [];
|
||||
|
||||
for (let idx = 0; idx < fields_order.length; idx++) {
|
||||
me.fields.push({
|
||||
fieldname: fields_order.item(idx).getAttribute("data-fieldname"),
|
||||
label: fields_order.item(idx).getAttribute("data-label")
|
||||
});
|
||||
}
|
||||
|
||||
me.dialog.set_value("fields", JSON.stringify(me.fields));
|
||||
me.dialog.get_value("fields");
|
||||
}
|
||||
|
||||
column_selector() {
|
||||
let me = this;
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("{0} Fields", [__(me.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
label: __("Reset Fields"),
|
||||
fieldtype: "Button",
|
||||
fieldname: "reset_fields",
|
||||
click: () => me.reset_listview_fields(d)
|
||||
},
|
||||
{
|
||||
label: __("Select Fields"),
|
||||
fieldtype: "MultiCheck",
|
||||
fieldname: "fields",
|
||||
options: me.get_doctype_fields(me.meta, me.fields.map(f => f.fieldname)),
|
||||
columns: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
d.set_primary_action(__('Save'), () => {
|
||||
let values = d.get_values().fields;
|
||||
|
||||
me.set_removed_fields(me.get_removed_listview_fields(values, me.fields.map(f => f.fieldname)));
|
||||
|
||||
me.fields = [];
|
||||
me.set_subject_field(me.meta);
|
||||
me.set_status_field();
|
||||
|
||||
for (let idx in values) {
|
||||
let value = values[idx];
|
||||
|
||||
if (me.fields.length === parseInt(me.dialog.get_values().total_fields)) {
|
||||
break;
|
||||
} else if (value != me.subject_field.fieldname) {
|
||||
let field = frappe.meta.get_docfield(me.doctype, value);
|
||||
if (field) {
|
||||
me.fields.push({
|
||||
label: field.label,
|
||||
fieldname: field.fieldname
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
me.refresh();
|
||||
me.dialog.set_value("fields", JSON.stringify(me.fields));
|
||||
d.hide();
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
|
||||
reset_listview_fields(dialog) {
|
||||
let me = this;
|
||||
|
||||
frappe.xcall("frappe.desk.doctype.list_view_settings.list_view_settings.get_default_listview_fields", {
|
||||
doctype: me.doctype
|
||||
}).then((fields) => {
|
||||
let field = dialog.get_field("fields");
|
||||
field.df.options = me.get_doctype_fields(me.meta, fields);
|
||||
dialog.refresh();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
get_listview_fields(meta) {
|
||||
let me = this;
|
||||
|
||||
if (!me.settings.fields) {
|
||||
me.set_list_view_fields(meta);
|
||||
} else {
|
||||
me.fields = JSON.parse(this.settings.fields);
|
||||
}
|
||||
|
||||
me.fields.uniqBy(f => f.fieldname);
|
||||
}
|
||||
|
||||
set_list_view_fields(meta) {
|
||||
let me = this;
|
||||
|
||||
me.set_subject_field(meta);
|
||||
me.set_status_field();
|
||||
|
||||
meta.fields.forEach(field => {
|
||||
if (field.in_list_view && !in_list(frappe.model.no_value_type, field.fieldtype) &&
|
||||
me.subject_field.fieldname != field.fieldname) {
|
||||
|
||||
me.fields.push({
|
||||
label: field.label,
|
||||
fieldname: field.fieldname
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_subject_field(meta) {
|
||||
let me = this;
|
||||
|
||||
me.subject_field = {
|
||||
label: "Name",
|
||||
fieldname: "name"
|
||||
};
|
||||
|
||||
if (meta.title_field) {
|
||||
let field = frappe.meta.get_docfield(me.doctype, meta.title_field.trim());
|
||||
|
||||
me.subject_field = {
|
||||
label: field.label,
|
||||
fieldname: field.fieldname
|
||||
};
|
||||
}
|
||||
|
||||
me.fields.push(me.subject_field);
|
||||
}
|
||||
|
||||
set_status_field() {
|
||||
let me = this;
|
||||
|
||||
if (frappe.has_indicator(me.doctype)) {
|
||||
me.fields.push({
|
||||
type: "Status",
|
||||
label: "Status",
|
||||
fieldname: "status_field"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_doctype_fields(meta, fields) {
|
||||
let multiselect_fields = [];
|
||||
|
||||
meta.fields.forEach(field => {
|
||||
if (!in_list(frappe.model.no_value_type, field.fieldtype)) {
|
||||
multiselect_fields.push({
|
||||
label: field.label,
|
||||
value: field.fieldname,
|
||||
checked: in_list(fields, field.fieldname)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return multiselect_fields;
|
||||
}
|
||||
|
||||
get_removed_listview_fields(new_fields, existing_fields) {
|
||||
let me = this;
|
||||
let removed_fields = [];
|
||||
|
||||
if (frappe.has_indicator(me.doctype)) {
|
||||
new_fields.push("status_field");
|
||||
}
|
||||
|
||||
existing_fields.forEach(column => {
|
||||
if (!in_list(new_fields, column)) {
|
||||
removed_fields.push(column);
|
||||
}
|
||||
});
|
||||
|
||||
return removed_fields;
|
||||
}
|
||||
|
||||
set_removed_fields(fields) {
|
||||
let me = this;
|
||||
|
||||
if (me.removed_fields) {
|
||||
me.removed_fields.concat(fields);
|
||||
} else {
|
||||
me.removed_fields = fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import BulkOperations from "./bulk_operations";
|
||||
import ListSettings from "./list_settings";
|
||||
|
||||
frappe.provide('frappe.views');
|
||||
|
||||
|
|
@ -231,6 +232,32 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
}
|
||||
|
||||
refresh_columns(meta, list_view_settings) {
|
||||
this.meta = meta;
|
||||
this.list_view_settings = list_view_settings;
|
||||
|
||||
this.setup_columns();
|
||||
this.refresh(true);
|
||||
}
|
||||
|
||||
refresh(refresh_header=false) {
|
||||
this.freeze(true);
|
||||
// fetch data from server
|
||||
return frappe.call(this.get_call_args()).then(r => {
|
||||
// render
|
||||
this.prepare_data(r);
|
||||
this.toggle_result_area();
|
||||
this.before_render();
|
||||
this.render_header(refresh_header);
|
||||
this.render();
|
||||
this.after_render();
|
||||
this.freeze(false);
|
||||
if (this.settings.refresh) {
|
||||
this.settings.refresh(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_freeze_area() {
|
||||
this.$freeze =
|
||||
$(`<div class="freeze flex justify-center align-center text-muted">${__('Loading')}...</div>`)
|
||||
|
|
@ -287,19 +314,49 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}))
|
||||
);
|
||||
|
||||
// limit max to 8 columns
|
||||
if (this.list_view_settings.fields) {
|
||||
this.columns = this.reorder_listview_fields();
|
||||
}
|
||||
|
||||
// limit max to 8 columns if no total_fields is set in List View Settings
|
||||
// Screen with low density no of columns 4
|
||||
// Screen with medium density no of columns 6
|
||||
// Screen with high density no of columns 8
|
||||
let column_count = 6;
|
||||
let total_fields = 6;
|
||||
|
||||
if (window.innerWidth <= 1200) {
|
||||
column_count = 4;
|
||||
} else if (window.innerWidth > 1440) {
|
||||
column_count = 8;
|
||||
if (window.innerWidth <= 1366) {
|
||||
total_fields = 4;
|
||||
} else if (window.innerWidth >= 1920) {
|
||||
total_fields = 8;
|
||||
}
|
||||
|
||||
this.columns = this.columns.slice(0, column_count);
|
||||
this.columns = this.columns.slice(0, this.list_view_settings.total_fields || total_fields);
|
||||
}
|
||||
|
||||
reorder_listview_fields() {
|
||||
let fields_order = [];
|
||||
let fields = JSON.parse(this.list_view_settings.fields);
|
||||
|
||||
//title_field is fixed
|
||||
fields_order.push(this.columns[0]);
|
||||
this.columns.splice(0, 1);
|
||||
|
||||
for (let fld in fields) {
|
||||
for (let col in this.columns) {
|
||||
let field = fields[fld];
|
||||
let column = this.columns[col];
|
||||
|
||||
if (column.type == "Status" && field.fieldname == "status_field") {
|
||||
fields_order.push(column);
|
||||
break;
|
||||
} else if (column.type == "Field" && field.fieldname === column.df.fieldname) {
|
||||
fields_order.push(column);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields_order;
|
||||
}
|
||||
|
||||
get_documentation_link() {
|
||||
|
|
@ -386,7 +443,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
}
|
||||
|
||||
render_header() {
|
||||
render_header(refresh_header=false) {
|
||||
if (refresh_header) {
|
||||
this.$result.find('.list-row-head').remove();
|
||||
}
|
||||
|
||||
if (this.$result.find('.list-row-head').length === 0) {
|
||||
// append header once
|
||||
this.$result.prepend(this.get_header_html());
|
||||
|
|
@ -1284,18 +1345,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
show_list_settings() {
|
||||
frappe.model.with_doctype("List View Setting", () => {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Settings"),
|
||||
fields: frappe.get_meta("List View Setting").fields
|
||||
});
|
||||
d.set_values(this.list_view_settings);
|
||||
d.show();
|
||||
d.set_primary_action(__('Save'), () => {
|
||||
let values = d.get_values();
|
||||
frappe.call("frappe.desk.listview.set_list_settings", {doctype: this.doctype, values: values});
|
||||
Object.assign(this.list_view_settings, values);
|
||||
d.hide();
|
||||
frappe.model.with_doctype(this.doctype, () => {
|
||||
new ListSettings({
|
||||
listview: this,
|
||||
doctype: this.doctype,
|
||||
settings: this.list_view_settings,
|
||||
meta: frappe.get_meta(this.doctype)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,7 +287,8 @@ frappe.socketio.SocketIOUploader = class SocketIOUploader {
|
|||
}
|
||||
|
||||
function fallback_required() {
|
||||
return !frappe.boot.sysdefaults.use_socketio_to_upload_file || !frappe.socketio.socket.connected;
|
||||
return !frappe.socketio.socket.connected
|
||||
|| !( !frappe.boot.sysdefaults || frappe.boot.sysdefaults.use_socketio_to_upload_file );
|
||||
}
|
||||
|
||||
if (fallback_required()) {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
this.body = this.$body.get(0);
|
||||
this.$message = $('<div class="hide modal-message"></div>').appendTo(this.modal_body);
|
||||
this.header = this.$wrapper.find(".modal-header");
|
||||
this.buttons = this.header.find('.buttons');
|
||||
this.set_indicator();
|
||||
|
||||
// make fields (if any)
|
||||
super.make();
|
||||
|
|
@ -164,6 +166,11 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
set_title(t) {
|
||||
this.$wrapper.find(".modal-title").html(t);
|
||||
}
|
||||
set_indicator() {
|
||||
if (this.indicator) {
|
||||
this.header.find('.indicator').removeClass().addClass('indicator ' + this.indicator);
|
||||
}
|
||||
}
|
||||
show() {
|
||||
// show it
|
||||
if ( this.animate ) {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,14 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
// child tables
|
||||
$.each(me.table_fields, function(i, table_df) {
|
||||
if(table_df.options) {
|
||||
var child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
|
||||
let child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
|
||||
|
||||
if (table_df.fieldtype === "Table MultiSelect") {
|
||||
const link_field = frappe.meta.get_docfields(table_df.options)
|
||||
.find(df => df.fieldtype === 'Link');
|
||||
child_table_fields = link_field ? [link_field] : [];
|
||||
}
|
||||
|
||||
$.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) {
|
||||
// show fields where user has read access and if report hide flag is not set
|
||||
if(frappe.perm.has_perm(me.doctype, df.permlevel, "read"))
|
||||
|
|
@ -130,15 +137,22 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
},
|
||||
|
||||
add_field_option(df) {
|
||||
if (df.fieldname == 'docstatus' && !frappe.model.is_submittable(this.doctype))
|
||||
let me = this;
|
||||
|
||||
if (df.fieldname == 'docstatus' && !frappe.model.is_submittable(me.doctype))
|
||||
return;
|
||||
|
||||
var me = this;
|
||||
var label, table;
|
||||
if (frappe.model.table_fields.includes(df.fieldtype)) {
|
||||
me.table_fields.push(df);
|
||||
return;
|
||||
}
|
||||
|
||||
let label = null;
|
||||
let table = null;
|
||||
|
||||
if(me.doctype && df.parent==me.doctype) {
|
||||
label = __(df.label);
|
||||
table = me.doctype;
|
||||
if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df);
|
||||
} else {
|
||||
label = __(df.label) + ' (' + __(df.parent) + ')';
|
||||
table = df.parent;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ frappe.ui.Filter = class {
|
|||
}
|
||||
|
||||
this.utils = frappe.ui.filter_utils;
|
||||
this.set_conditions();
|
||||
this.set_conditions_from_config();
|
||||
this.make();
|
||||
}
|
||||
|
||||
set_conditions() {
|
||||
this.conditions = [
|
||||
["=", __("Equals")],
|
||||
["!=", __("Not Equals")],
|
||||
|
|
@ -19,8 +25,7 @@ frappe.ui.Filter = class {
|
|||
[">=", ">="],
|
||||
["<=", "<="],
|
||||
["Between", __("Between")],
|
||||
["Previous", __("Previous")],
|
||||
["Next", __("Next")]
|
||||
["Timespan", __("Timespan")],
|
||||
];
|
||||
|
||||
this.nested_set_conditions = [
|
||||
|
|
@ -35,17 +40,28 @@ frappe.ui.Filter = class {
|
|||
this.invalid_condition_map = {
|
||||
Date: ['like', 'not like'],
|
||||
Datetime: ['like', 'not like'],
|
||||
Data: ['Between', 'Previous', 'Next'],
|
||||
Select: ['like', 'not like', 'Between', 'Previous', 'Next'],
|
||||
Link: ["Between", 'Previous', 'Next', '>', '<', '>=', '<='],
|
||||
Currency: ["Between", 'Previous', 'Next'],
|
||||
Color: ["Between", 'Previous', 'Next'],
|
||||
Data: ['Between', 'Timespan'],
|
||||
Select: ['like', 'not like', 'Between', 'Timespan'],
|
||||
Link: ["Between", 'Timespan', '>', '<', '>=', '<='],
|
||||
Currency: ["Between", 'Timespan'],
|
||||
Color: ["Between", 'Timespan'],
|
||||
Check: this.conditions.map(c => c[0]).filter(c => c !== '=')
|
||||
};
|
||||
this.make();
|
||||
this.make_select();
|
||||
this.set_events();
|
||||
this.setup();
|
||||
}
|
||||
|
||||
set_conditions_from_config() {
|
||||
if (frappe.boot.additional_filters_config) {
|
||||
this.filters_config = frappe.boot.additional_filters_config;
|
||||
for (let key of Object.keys(this.filters_config)) {
|
||||
const filter = this.filters_config[key];
|
||||
this.conditions.push([key, __(`{0}`, [filter.label])]);
|
||||
for (let fieldtype of Object.keys(this.invalid_condition_map)) {
|
||||
if (!filter.valid_for_fieldtypes.includes(fieldtype)) {
|
||||
this.invalid_condition_map[fieldtype].push(filter.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
|
|
@ -53,6 +69,10 @@ frappe.ui.Filter = class {
|
|||
conditions: this.conditions
|
||||
}))
|
||||
.appendTo(this.parent.find('.filter-edit-area'));
|
||||
|
||||
this.make_select();
|
||||
this.set_events();
|
||||
this.setup();
|
||||
}
|
||||
|
||||
make_select() {
|
||||
|
|
@ -203,33 +223,23 @@ frappe.ui.Filter = class {
|
|||
this.fieldselect.selected_doctype = doctype;
|
||||
this.fieldselect.selected_fieldname = fieldname;
|
||||
|
||||
if(["Previous", "Next"].includes(condition) && ['Date', 'Datetime', 'DateRange', 'Select'].includes(this.field.df.fieldtype)) {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = [
|
||||
{
|
||||
label: __('1 week'),
|
||||
value: '1 week'
|
||||
},
|
||||
{
|
||||
label: __('1 month'),
|
||||
value: '1 month'
|
||||
},
|
||||
{
|
||||
label: __('3 months'),
|
||||
value: '3 months'
|
||||
},
|
||||
{
|
||||
label: __('6 months'),
|
||||
value: '6 months'
|
||||
},
|
||||
{
|
||||
label: __('1 year'),
|
||||
value: '1 year'
|
||||
}
|
||||
];
|
||||
if (this.filters_config && this.filters_config[condition]
|
||||
&& this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) {
|
||||
let args = {};
|
||||
if (this.filters_config[condition].depends_on) {
|
||||
const field_name = this.filters_config[condition].depends_on;
|
||||
const filter_value = this.base_list.get_filter_value(field_name);
|
||||
args[field_name] = filter_value;
|
||||
}
|
||||
frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
});
|
||||
} else {
|
||||
this.make_field(df, cur.fieldtype);
|
||||
}
|
||||
|
||||
this.make_field(df, cur.fieldtype);
|
||||
}
|
||||
|
||||
make_field(df, old_fieldtype) {
|
||||
|
|
@ -440,6 +450,10 @@ frappe.ui.filter_utils = {
|
|||
if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
|
||||
df.fieldtype = 'DateRange';
|
||||
}
|
||||
if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
|
||||
}
|
||||
if (condition === 'is') {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = [
|
||||
|
|
@ -447,5 +461,32 @@ frappe.ui.filter_utils = {
|
|||
{ label: __('Not Set'), value: 'not set' },
|
||||
];
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
get_timespan_options(periods) {
|
||||
const period_map = {
|
||||
'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
'Today': null,
|
||||
'This': ['Week', 'Month', 'Quarter', 'Year'],
|
||||
'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year']
|
||||
};
|
||||
let options = [];
|
||||
periods.forEach(period => {
|
||||
if (period_map[period]) {
|
||||
period_map[period].forEach(p => {
|
||||
options.push({
|
||||
label: __(`{0} {1}`, [period, p]),
|
||||
value: `${period.toLowerCase()} ${p.toLowerCase()}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
options.push({
|
||||
label: __(`{0}`, [period]),
|
||||
value: `${period.toLowerCase()}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ frappe.ui.FilterGroup = class {
|
|||
},
|
||||
filter_items: (doctype, fieldname) => {
|
||||
return !this.filter_exists([doctype, fieldname]);
|
||||
}
|
||||
},
|
||||
base_list: this.base_list
|
||||
};
|
||||
let filter = new frappe.ui.Filter(args);
|
||||
this.filters.push(filter);
|
||||
|
|
@ -132,7 +133,6 @@ frappe.ui.FilterGroup = class {
|
|||
|
||||
get_filters() {
|
||||
return this.filters.filter(f => f.field).map(f => {
|
||||
f.freeze();
|
||||
return f.get_value();
|
||||
});
|
||||
// {}: this.list.update_standard_filters(values);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,33 @@ frappe.confirm = function(message, ifyes, ifno) {
|
|||
return d;
|
||||
}
|
||||
|
||||
frappe.warn = function(title, message_html, proceed_action, primary_label) {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
indicator: 'red',
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'warning_message',
|
||||
options: `<div class="frappe-warning-message">${message_html}</div>`
|
||||
}
|
||||
],
|
||||
primary_action_label: primary_label,
|
||||
primary_action: () => {
|
||||
if (proceed_action) proceed_action();
|
||||
d.hide();
|
||||
},
|
||||
secondary_action_label: __("Cancel"),
|
||||
});
|
||||
|
||||
d.buttons.find('.btn-primary').removeClass('btn-primary').addClass('btn-danger');
|
||||
const modal_footer = $(`<div class="modal-footer"></div>`).insertAfter($(d.modal_body));
|
||||
modal_footer.html(d.buttons);
|
||||
|
||||
d.show();
|
||||
return d;
|
||||
};
|
||||
|
||||
frappe.prompt = function(fields, callback, title, primary_label) {
|
||||
if (typeof fields === "string") {
|
||||
fields = [{
|
||||
|
|
|
|||
|
|
@ -153,12 +153,12 @@ frappe.ui.Notifications = class Notifications {
|
|||
let title = target ? `title="${__('Your Target')}"` : '';
|
||||
let $list_item = !target
|
||||
? $(`<li><a class="badge-hover" data-action="route_to_document_type" data-doctype="${name}" ${title}>
|
||||
${label}
|
||||
${__(label)}
|
||||
<span class="badge pull-right">${value}</span>
|
||||
</a></li>`)
|
||||
: $(`<li><a class="progress-small" data-action="route_to_document_type" ${title}
|
||||
data-doctype="${doc_dt}" data-docname="${name}">
|
||||
<span class="dropdown-item-label">${label}<span>
|
||||
<span class="dropdown-item-label">${__(label)}<span>
|
||||
<div class="progress-chart">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: ${value}%"></div>
|
||||
|
|
@ -304,10 +304,7 @@ frappe.ui.Notifications = class Notifications {
|
|||
}
|
||||
|
||||
get_dropdown_item_html(field) {
|
||||
let doc_link = frappe.utils.get_form_link(
|
||||
field.document_type,
|
||||
field.document_name
|
||||
);
|
||||
let doc_link = this.get_item_link(field);
|
||||
let read_class = field.read ? '' : 'unread';
|
||||
let mark_read_action = field.read ? '': 'data-action="mark_as_read"';
|
||||
let message = field.subject;
|
||||
|
|
@ -336,6 +333,17 @@ frappe.ui.Notifications = class Notifications {
|
|||
return item_html;
|
||||
}
|
||||
|
||||
get_item_link(notification_doc) {
|
||||
const link_doctype =
|
||||
notification_doc.type == 'Alert' ? 'Notification Log': notification_doc.document_type;
|
||||
const link_docname =
|
||||
notification_doc.type == 'Alert' ? notification_doc.name: notification_doc.document_name;
|
||||
return frappe.utils.get_form_link(
|
||||
link_doctype,
|
||||
link_docname
|
||||
);
|
||||
}
|
||||
|
||||
render_dropdown_headers() {
|
||||
this.categories = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -292,6 +292,25 @@ Object.assign(frappe.utils, {
|
|||
return frappe.utils.guess_style(text, null, true);
|
||||
},
|
||||
|
||||
get_indicator_color: function(state) {
|
||||
return frappe.db.get_list('Workflow State', {filters: {name: state}, fields: ['name', 'style']}).then(res => {
|
||||
const state = res[0];
|
||||
if (!state.style) {
|
||||
return frappe.utils.guess_colour(state.name);
|
||||
}
|
||||
const style = state.style;
|
||||
const colour_map = {
|
||||
"Success": "green",
|
||||
"Warning": "orange",
|
||||
"Danger": "red",
|
||||
"Primary": "blue",
|
||||
};
|
||||
|
||||
return colour_map[style];
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
sort: function(list, key, compare_type, reverse) {
|
||||
if(!list || list.length < 2)
|
||||
return list || [];
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
frappe.ui.form.on("Web Page Block", {
|
||||
edit_values(frm, cdt, cdn) {
|
||||
let row = frm.selected_doc;
|
||||
frappe.model.with_doc("Web Template", row.web_template).then((doc) => {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Edit Values"),
|
||||
fields: doc.fields.map((df) => {
|
||||
if (df.fieldtype == "Section Break") {
|
||||
df.collapsible = 1;
|
||||
}
|
||||
return df;
|
||||
}),
|
||||
primary_action(values) {
|
||||
frappe.model.set_value(
|
||||
cdt,
|
||||
cdn,
|
||||
"web_template_values",
|
||||
JSON.stringify(values)
|
||||
);
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
let values = JSON.parse(row.web_template_values || "{}");
|
||||
d.set_values(values);
|
||||
d.show();
|
||||
|
||||
d.sections.forEach((sect) => {
|
||||
let fields_with_value = sect.fields_list.filter(
|
||||
(field) => values[field.df.fieldname]
|
||||
);
|
||||
|
||||
if (fields_with_value.length) {
|
||||
sect.collapse(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -89,16 +89,21 @@ frappe.breadcrumbs = {
|
|||
breadcrumbs.module = frappe.breadcrumbs.module_map[breadcrumbs.module];
|
||||
}
|
||||
|
||||
if(frappe.get_module(breadcrumbs.module)) {
|
||||
let current_module = breadcrumbs.module
|
||||
// Check if a desk page exists
|
||||
if (frappe.boot.module_page_map[breadcrumbs.module]) {
|
||||
breadcrumbs.module = frappe.boot.module_page_map[breadcrumbs.module];
|
||||
}
|
||||
|
||||
if(frappe.get_module(current_module)) {
|
||||
// if module access exists
|
||||
var module_info = frappe.get_module(breadcrumbs.module),
|
||||
var module_info = frappe.get_module(current_module),
|
||||
icon = module_info && module_info.icon,
|
||||
label = module_info ? module_info.label : breadcrumbs.module;
|
||||
|
||||
|
||||
if(module_info && !module_info.blocked && frappe.visible_modules.includes(module_info.module_name)) {
|
||||
$(repl('<li><a href="#workspace/%(module)s">%(label)s</a></li>',
|
||||
{ module: breadcrumbs.module, label: __(label) }))
|
||||
{ module: breadcrumbs.module, label: __(breadcrumbs.module) }))
|
||||
.appendTo($breadcrumbs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ function go_to_list_with_filters(doctype, filters) {
|
|||
}
|
||||
|
||||
function shorten_number(number, country) {
|
||||
country = country || '';
|
||||
country = (country == 'India') ? country : '';
|
||||
const number_system = get_number_system(country);
|
||||
let x = Math.abs(Math.round(number));
|
||||
for (const map of number_system) {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ from frappe.desk.listview import get_list_settings, set_list_settings, get_group
|
|||
|
||||
class TestListView(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if frappe.db.exists("List View Setting", "DocType"):
|
||||
frappe.delete_doc("List View Setting", "DocType")
|
||||
if frappe.db.exists("List View Settings", "DocType"):
|
||||
frappe.delete_doc("List View Settings", "DocType")
|
||||
|
||||
def test_get_list_settings_without_settings(self):
|
||||
self.assertIsNone(get_list_settings("DocType"), None)
|
||||
|
||||
def test_get_list_settings_with_default_settings(self):
|
||||
frappe.get_doc({"doctype": "List View Setting", "name": "DocType"}).insert()
|
||||
frappe.get_doc({"doctype": "List View Settings", "name": "DocType"}).insert()
|
||||
settings = get_list_settings("DocType")
|
||||
self.assertIsNotNone(settings)
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ class TestListView(unittest.TestCase):
|
|||
self.assertEqual(settings.disable_sidebar_stats, 0)
|
||||
|
||||
def test_get_list_settings_with_non_default_settings(self):
|
||||
frappe.get_doc({"doctype": "List View Setting", "name": "DocType", "disable_count": 1}).insert()
|
||||
frappe.get_doc({"doctype": "List View Settings", "name": "DocType", "disable_count": 1}).insert()
|
||||
settings = get_list_settings("DocType")
|
||||
self.assertIsNotNone(settings)
|
||||
|
||||
|
|
@ -36,16 +36,16 @@ class TestListView(unittest.TestCase):
|
|||
|
||||
def test_set_list_settings_without_settings(self):
|
||||
set_list_settings("DocType", json.dumps({}))
|
||||
settings = frappe.get_doc("List View Setting","DocType")
|
||||
settings = frappe.get_doc("List View Settings","DocType")
|
||||
|
||||
self.assertEqual(settings.disable_auto_refresh, 0)
|
||||
self.assertEqual(settings.disable_count, 0)
|
||||
self.assertEqual(settings.disable_sidebar_stats, 0)
|
||||
|
||||
def test_set_list_settings_with_existing_settings(self):
|
||||
frappe.get_doc({"doctype": "List View Setting", "name": "DocType", "disable_count": 1}).insert()
|
||||
frappe.get_doc({"doctype": "List View Settings", "name": "DocType", "disable_count": 1}).insert()
|
||||
set_list_settings("DocType", json.dumps({"disable_count": 0, "disable_auto_refresh": 1}))
|
||||
settings = frappe.get_doc("List View Setting","DocType")
|
||||
settings = frappe.get_doc("List View Settings","DocType")
|
||||
|
||||
self.assertEqual(settings.disable_auto_refresh, 1)
|
||||
self.assertEqual(settings.disable_count, 0)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
from werkzeug.test import Client
|
||||
import os, re, sys, json, hashlib, requests, traceback
|
||||
import functools
|
||||
from .html_utils import sanitize_html
|
||||
import frappe
|
||||
from frappe.utils.identicon import Identicon
|
||||
|
|
@ -360,6 +361,7 @@ def decode_dict(d, encoding="utf-8"):
|
|||
|
||||
return d
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_site_name(hostname):
|
||||
return hostname.split(':')[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -50,14 +50,16 @@ def app_group(ctx, site=False, force=False, verbose=False, profile=False):
|
|||
ctx.info_name = ''
|
||||
|
||||
def get_sites(site_arg):
|
||||
if site_arg and site_arg == 'all':
|
||||
if site_arg == 'all':
|
||||
return frappe.utils.get_sites()
|
||||
else:
|
||||
if site_arg:
|
||||
return [site_arg]
|
||||
if os.path.exists('currentsite.txt'):
|
||||
with open('currentsite.txt') as f:
|
||||
return [f.read().strip()]
|
||||
elif site_arg:
|
||||
return [site_arg]
|
||||
elif os.path.exists('currentsite.txt'):
|
||||
with open('currentsite.txt') as f:
|
||||
site = f.read().strip()
|
||||
if site:
|
||||
return [site]
|
||||
return []
|
||||
|
||||
def get_app_commands(app):
|
||||
if os.path.exists(os.path.join('..', 'apps', app, app, 'commands.py'))\
|
||||
|
|
|
|||
|
|
@ -27,6 +27,15 @@ def add_line_after(function):
|
|||
return empty_line
|
||||
|
||||
|
||||
def add_line_before(function):
|
||||
"""Adds an extra line to STDOUT before the execution of a function this decorates"""
|
||||
def empty_line(*args, **kwargs):
|
||||
print()
|
||||
result = function(*args, **kwargs)
|
||||
return result
|
||||
return empty_line
|
||||
|
||||
|
||||
def log(message, colour=''):
|
||||
"""Coloured log outputs to STDOUT"""
|
||||
colours = {
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ def nowtime():
|
|||
"""return current time in hh:mm"""
|
||||
return now_datetime().strftime(TIME_FORMAT)
|
||||
|
||||
def get_first_day(dt, d_years=0, d_months=0):
|
||||
def get_first_day(dt, d_years=0, d_months=0, as_str=False):
|
||||
"""
|
||||
Returns the first day of the month for the date specified by date object
|
||||
Also adds `d_years` and `d_months` if specified
|
||||
|
|
@ -185,10 +185,23 @@ def get_first_day(dt, d_years=0, d_months=0):
|
|||
overflow_years, month = divmod(dt.month + d_months - 1, 12)
|
||||
year = dt.year + d_years + overflow_years
|
||||
|
||||
return datetime.date(year, month + 1, 1)
|
||||
return datetime.date(year, month + 1, 1).strftime(DATE_FORMAT) if as_str else datetime.date(year, month + 1, 1)
|
||||
|
||||
def get_first_day_of_week(dt):
|
||||
return dt - datetime.timedelta(days=dt.weekday())
|
||||
def get_quarter_start(dt, as_str=False):
|
||||
date = getdate(dt)
|
||||
quarter = (date.month - 1) // 3 + 1
|
||||
first_date_of_quarter = datetime.date(date.year, ((quarter - 1) * 3) + 1, 1)
|
||||
return first_date_of_quarter.strftime(DATE_FORMAT) if as_str else first_date_of_quarter
|
||||
|
||||
def get_first_day_of_week(dt, as_str=False):
|
||||
dt = getdate(dt)
|
||||
date = dt - datetime.timedelta(days=dt.weekday())
|
||||
return date.strftime(DATE_FORMAT) if as_str else date
|
||||
|
||||
def get_year_start(dt, as_str=False):
|
||||
dt = getdate(dt)
|
||||
date = datetime.date(dt.year, 1, 1)
|
||||
return date.strftime(DATE_FORMAT) if as_str else date
|
||||
|
||||
def get_last_day_of_week(dt):
|
||||
dt = get_first_day_of_week(dt)
|
||||
|
|
@ -360,6 +373,27 @@ def get_weekday(datetime=None):
|
|||
weekdays = get_weekdays()
|
||||
return weekdays[datetime.weekday()]
|
||||
|
||||
def get_timespan_date_range(timespan):
|
||||
date_range_map = {
|
||||
"last week": [add_to_date(nowdate(), days=-7), nowdate()],
|
||||
"last month": [add_to_date(nowdate(), months=-1), nowdate()],
|
||||
"last quarter": [add_to_date(nowdate(), months=-3), nowdate()],
|
||||
"last 6 months": [add_to_date(nowdate(), months=-6), nowdate()],
|
||||
"last year": [add_to_date(nowdate(), years=-1), nowdate()],
|
||||
"today": [nowdate(), nowdate()],
|
||||
"this week": [get_first_day_of_week(nowdate(), as_str=True), nowdate()],
|
||||
"this month": [get_first_day(nowdate(), as_str=True), nowdate()],
|
||||
"this quarter": [get_quarter_start(nowdate(), as_str=True), nowdate()],
|
||||
"this year": [get_year_start(nowdate(), as_str=True), nowdate()],
|
||||
"next week": [nowdate(), add_to_date(nowdate(), days=7)],
|
||||
"next month": [nowdate(), add_to_date(nowdate(), months=1)],
|
||||
"next quarter": [nowdate(), add_to_date(nowdate(), months=3)],
|
||||
"next 6 months": [nowdate(), add_to_date(nowdate(), months=6)],
|
||||
"next year": [nowdate(), add_to_date(nowdate(), years=1)],
|
||||
}
|
||||
|
||||
return date_range_map.get(timespan)
|
||||
|
||||
def global_date_format(date, format="long"):
|
||||
"""returns localized date in the form of January 1, 2012"""
|
||||
date = getdate(date)
|
||||
|
|
@ -998,7 +1032,7 @@ def compare(val1, condition, val2):
|
|||
|
||||
return ret
|
||||
|
||||
def get_filter(doctype, f):
|
||||
def get_filter(doctype, f, filters_config=None):
|
||||
"""Returns a _dict like
|
||||
|
||||
{
|
||||
|
|
@ -1033,7 +1067,15 @@ def get_filter(doctype, f):
|
|||
f.operator = "="
|
||||
|
||||
valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "is",
|
||||
"between", "descendants of", "ancestors of", "not descendants of", "not ancestors of", "previous", "next")
|
||||
"between", "descendants of", "ancestors of", "not descendants of", "not ancestors of",
|
||||
"timespan", "previous", "next")
|
||||
|
||||
if filters_config:
|
||||
additional_operators = []
|
||||
for key in filters_config:
|
||||
additional_operators.append(key.lower())
|
||||
valid_operators = tuple(set(valid_operators + tuple(additional_operators)))
|
||||
|
||||
if f.operator.lower() not in valid_operators:
|
||||
frappe.throw(frappe._("Operator must be one of {0}").format(", ".join(valid_operators)))
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def make_error_snapshot(exception):
|
|||
if frappe.conf.disable_error_snapshot:
|
||||
return
|
||||
|
||||
logger = frappe.logger(__name__, with_more_info=False)
|
||||
logger = frappe.logger(with_more_info=True)
|
||||
|
||||
try:
|
||||
error_id = '{timestamp:s}-{ip:s}-{hash:s}'.format(
|
||||
|
|
@ -123,22 +123,13 @@ def get_snapshot(exception, context=10):
|
|||
# add exception type, value and attributes
|
||||
if isinstance(evalue, BaseException):
|
||||
for name in dir(evalue):
|
||||
# prevent py26 DeprecationWarning
|
||||
if (name != 'messages' or sys.version_info < (2.6)) and not name.startswith('__'):
|
||||
if name != 'messages' and not name.startswith('__'):
|
||||
value = pydoc.text.repr(getattr(evalue, name))
|
||||
|
||||
# render multilingual string properly
|
||||
if isinstance(value, six.text_type):
|
||||
value = eval(value)
|
||||
|
||||
s['exception'][name] = encode(value)
|
||||
|
||||
# add all local values (of last frame) to the snapshot
|
||||
for name, value in locals.items():
|
||||
if isinstance(value, six.text_type):
|
||||
value = eval(value)
|
||||
|
||||
s['locals'][name] = pydoc.text.repr(value)
|
||||
s['locals'][name] = value if isinstance(value, six.text_type) else pydoc.text.repr(value)
|
||||
|
||||
return s
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,53 @@
|
|||
# imports - compatibility imports
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
# imports - standard imports
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# imports - third party imports
|
||||
from six import text_type
|
||||
|
||||
default_log_level = logging.DEBUG
|
||||
LOG_FILENAME = '../logs/{}-frappe.log'.format(frappe.local.site)
|
||||
# imports - module imports
|
||||
import frappe
|
||||
|
||||
def get_logger(module, with_more_info=True):
|
||||
|
||||
default_log_level = logging.DEBUG
|
||||
site = getattr(frappe.local, 'site', None)
|
||||
|
||||
|
||||
def get_logger(module, with_more_info=False):
|
||||
global site
|
||||
if module in frappe.loggers:
|
||||
return frappe.loggers[module]
|
||||
|
||||
formatter = logging.Formatter('[%(levelname)s] %(asctime)s | %(pathname)s:\n%(message)s')
|
||||
# handler = logging.StreamHandler()
|
||||
if not module:
|
||||
module = "frappe"
|
||||
with_more_info = True
|
||||
|
||||
handler = RotatingFileHandler(
|
||||
LOG_FILENAME, maxBytes=100000, backupCount=20)
|
||||
handler.setFormatter(formatter)
|
||||
logfile = module + '.log'
|
||||
site = getattr(frappe.local, 'site', None)
|
||||
LOG_FILENAME = os.path.join('..', 'logs', logfile)
|
||||
|
||||
logger = logging.getLogger(module)
|
||||
logger.setLevel(frappe.log_level or default_log_level)
|
||||
logger.propagate = False
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s')
|
||||
handler = RotatingFileHandler(LOG_FILENAME, maxBytes=100_000, backupCount=20)
|
||||
logger.addHandler(handler)
|
||||
#
|
||||
if site:
|
||||
SITELOG_FILENAME = os.path.join(site, 'logs', logfile)
|
||||
site_handler = RotatingFileHandler(SITELOG_FILENAME, maxBytes=100_000, backupCount=20)
|
||||
site_handler.setFormatter(formatter)
|
||||
logger.addHandler(site_handler)
|
||||
|
||||
if with_more_info:
|
||||
handler.addFilter(SiteContextFilter())
|
||||
|
||||
logger = logging.getLogger(module)
|
||||
logger.setLevel(frappe.log_level or default_log_level)
|
||||
logger.addHandler(handler)
|
||||
logger.propagate = False
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
frappe.loggers[module] = logger
|
||||
|
||||
|
|
@ -33,25 +56,9 @@ def get_logger(module, with_more_info=True):
|
|||
class SiteContextFilter(logging.Filter):
|
||||
"""This is a filter which injects request information (if available) into the log."""
|
||||
def filter(self, record):
|
||||
record.msg = get_more_info_for_log() + text_type(record.msg)
|
||||
return True
|
||||
|
||||
def get_more_info_for_log():
|
||||
'''Adds Site, Form Dict into log entry'''
|
||||
more_info = []
|
||||
site = getattr(frappe.local, 'site', None)
|
||||
if site:
|
||||
more_info.append('Site: {0}'.format(site))
|
||||
|
||||
form_dict = getattr(frappe.local, 'form_dict', None)
|
||||
if form_dict:
|
||||
more_info.append('Form Dict: {0}'.format(frappe.as_json(form_dict)))
|
||||
|
||||
if more_info:
|
||||
# to append a \n
|
||||
more_info = more_info + ['']
|
||||
|
||||
return '\n'.join(more_info)
|
||||
if "Form Dict" not in text_type(record.msg):
|
||||
record.msg = text_type(record.msg) + "\nSite: {0}\nForm Dict: {1}".format(site, getattr(frappe.local, 'form_dict', None))
|
||||
return True
|
||||
|
||||
def set_log_level(level):
|
||||
'''Use this method to set log level to something other than the default DEBUG'''
|
||||
|
|
|
|||
|
|
@ -7,17 +7,24 @@ Events:
|
|||
monthly
|
||||
weekly
|
||||
"""
|
||||
# imports - compatibility imports
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
# imports - standard imports
|
||||
import os
|
||||
import time
|
||||
|
||||
import frappe, os, time
|
||||
# imports - third party imports
|
||||
import schedule
|
||||
from frappe.utils import now_datetime, get_datetime
|
||||
from frappe.utils import get_sites
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.core.doctype.user.user import STANDARD_USERS
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import get_sites, now_datetime
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
||||
|
||||
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
def start_scheduler():
|
||||
|
|
@ -48,9 +55,8 @@ def enqueue_events_for_all_sites():
|
|||
|
||||
def enqueue_events_for_site(site):
|
||||
def log_and_raise():
|
||||
frappe.logger(__name__).error('Exception in Enqueue Events for Site {0}'.format(site) +
|
||||
'\n' + frappe.get_traceback())
|
||||
raise # pylint: disable=misplaced-bare-raise
|
||||
error_message = 'Exception in Enqueue Events for Site {0}\n{1}'.format(site, frappe.get_traceback())
|
||||
frappe.logger("scheduler").error(error_message)
|
||||
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -60,10 +66,10 @@ def enqueue_events_for_site(site):
|
|||
|
||||
enqueue_events(site=site)
|
||||
|
||||
frappe.logger(__name__).debug('Queued events for site {0}'.format(site))
|
||||
frappe.logger("scheduler").debug('Queued events for site {0}'.format(site))
|
||||
except frappe.db.OperationalError as e:
|
||||
if frappe.db.is_access_denied(e):
|
||||
frappe.logger(__name__).debug('Access denied for site {0}'.format(site))
|
||||
frappe.logger("scheduler").debug('Access denied for site {0}'.format(site))
|
||||
else:
|
||||
log_and_raise()
|
||||
except:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
// MIT License. See license.txt
|
||||
|
||||
frappe.ui.form.on('Web Page', {
|
||||
onload: function() {
|
||||
frappe.require('/assets/frappe/js/frappe/utils/web_page_block.js');
|
||||
},
|
||||
title: function(frm) {
|
||||
if (frm.doc.title && !frm.doc.route) {
|
||||
frm.set_value('route', frappe.scrub(frm.doc.title, '-'));
|
||||
|
|
@ -49,6 +46,45 @@ frappe.ui.form.on('Web Page', {
|
|||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Web Page Block", {
|
||||
edit_values(frm, cdt, cdn) {
|
||||
let row = frm.selected_doc;
|
||||
frappe.model.with_doc("Web Template", row.web_template).then((doc) => {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Edit Values"),
|
||||
fields: doc.fields.map((df) => {
|
||||
if (df.fieldtype == "Section Break") {
|
||||
df.collapsible = 1;
|
||||
}
|
||||
return df;
|
||||
}),
|
||||
primary_action(values) {
|
||||
frappe.model.set_value(
|
||||
cdt,
|
||||
cdn,
|
||||
"web_template_values",
|
||||
JSON.stringify(values)
|
||||
);
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
let values = JSON.parse(row.web_template_values || "{}");
|
||||
d.set_values(values);
|
||||
d.show();
|
||||
|
||||
d.sections.forEach((sect) => {
|
||||
let fields_with_value = sect.fields_list.filter(
|
||||
(field) => values[field.df.fieldname]
|
||||
);
|
||||
|
||||
if (fields_with_value.length) {
|
||||
sect.collapse(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.tour['Web Page'] = [
|
||||
{
|
||||
fieldname: "title",
|
||||
|
|
@ -102,4 +138,4 @@ frappe.tour['Web Page'] = [
|
|||
title: __("Meta Image"),
|
||||
description: __("The meta image is unique image representing the content of the page. Images for this Card should be at least 280px in width, and at least 150px in height.")
|
||||
},
|
||||
];
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
// MIT License. See license.txt
|
||||
|
||||
frappe.ui.form.on('Website Theme', {
|
||||
onload: function() {
|
||||
frappe.require('/assets/frappe/js/frappe/utils/web_page_block.js');
|
||||
},
|
||||
refresh(frm) {
|
||||
frm.clear_custom_buttons();
|
||||
frm.toggle_display(["module", "custom"], !frappe.boot.developer_mode);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,35 @@ frappe.ui.form.on("Workflow", {
|
|||
frm.set_query("document_type", {"issingle": 0, "istable": 0});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.document_type) {
|
||||
frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), () => {
|
||||
frappe.set_route('List', frm.doc.document_type);
|
||||
});
|
||||
}
|
||||
|
||||
frm.events.update_field_options(frm);
|
||||
frm.ignore_warning = frm.is_new() ? true : false;
|
||||
|
||||
if (frm.is_new()) {
|
||||
return;
|
||||
}
|
||||
|
||||
frm.states = null;
|
||||
frm.trigger('make_state_table');
|
||||
frm.trigger('get_orphaned_states_and_count').then(() => {
|
||||
frm.trigger('render_state_table');
|
||||
});
|
||||
},
|
||||
validate: (frm) => {
|
||||
if (frm.ignore_warning) {
|
||||
return;
|
||||
}
|
||||
return frm.trigger('get_orphaned_states_and_count').then(() => {
|
||||
if (frm.states && frm.states.length) {
|
||||
frappe.validated = false;
|
||||
frm.trigger('create_warning_dialog');
|
||||
}
|
||||
});
|
||||
},
|
||||
document_type: function(frm) {
|
||||
frm.events.update_field_options(frm);
|
||||
|
|
@ -19,6 +47,101 @@ frappe.ui.form.on("Workflow", {
|
|||
frappe.meta.get_docfield("Workflow Document State", "update_field", frm.doc.name).options = [""].concat(resp);
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
create_warning_dialog: function(frm) {
|
||||
const warning_html =
|
||||
`<p class="bold">
|
||||
${__('Are you sure you want to save this document?')}
|
||||
</p>
|
||||
<p>${__(`There are documents which have workflow states that do not exist in this Workflow.
|
||||
It is recommended that you add these states to the Workflow and change their states
|
||||
before removing these states.`)}
|
||||
</p>`;
|
||||
const message_html = warning_html + frm.state_table_html;
|
||||
let proceed_action = () => {
|
||||
frm.ignore_warning = true;
|
||||
frm.save();
|
||||
};
|
||||
|
||||
frappe.warn(__(`Worflow States Don't Exist`), message_html, proceed_action, __(`Save Anyway`));
|
||||
},
|
||||
set_table_html: function(frm) {
|
||||
|
||||
const promises = frm.states.map(r => {
|
||||
const state = r[frm.doc.workflow_state_field];
|
||||
return frappe.utils.get_indicator_color(state).then(color => {
|
||||
return `<tr>
|
||||
<td>
|
||||
<div class="indicator ${color}">
|
||||
<a class="text-muted orphaned-state">${r[frm.doc.workflow_state_field]}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>${r.count}</td></tr>`;
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises).then(rows => {
|
||||
const rows_html = rows.join('');
|
||||
frm.state_table_html = (`<table class="table state-table table-bordered" style="margin:0px; width: 65%">
|
||||
<thead style="font-size: 12px">
|
||||
<tr class="text-muted">
|
||||
<th>${__('State')}</th>
|
||||
<th>${__('Count')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows_html}
|
||||
</tbody>
|
||||
</table>`);
|
||||
});
|
||||
},
|
||||
get_orphaned_states_and_count: function(frm) {
|
||||
let states_list = [];
|
||||
frm.doc.states.map(state => states_list.push(state.state));
|
||||
return frappe.xcall('frappe.workflow.doctype.workflow.workflow.get_workflow_state_count', {
|
||||
doctype: frm.doc.document_type,
|
||||
workflow_state_field: frm.doc.workflow_state_field,
|
||||
states: states_list
|
||||
}).then(result => {
|
||||
if (result && result.length) {
|
||||
frm.states = result;
|
||||
return frm.trigger('set_table_html');
|
||||
}
|
||||
});
|
||||
},
|
||||
make_state_table: function(frm) {
|
||||
const wrapper = frm.get_field('states').$wrapper;
|
||||
if (frm.state_table) {
|
||||
frm.state_table.empty();
|
||||
}
|
||||
frm.state_table = $(`<div class="state-table"><div>`).insertAfter(wrapper);
|
||||
},
|
||||
render_state_table: function(frm) {
|
||||
if (frm.states && frm.states.length) {
|
||||
const form_state_table_html =
|
||||
`<p class="text-muted small" style="margin-top: 30px">
|
||||
${'Document States that do not exist in your Workflow'}
|
||||
</p>
|
||||
${frm.state_table_html}
|
||||
</div>`;
|
||||
frm.state_table.html(form_state_table_html);
|
||||
|
||||
$(frm.state_table).find('a.orphaned-state').on('click', (e) => {
|
||||
const state = $(e.currentTarget).text();
|
||||
let filters = {};
|
||||
filters[frm.doc.workflow_state_field] = state;
|
||||
frappe.set_route('List', frm.doc.document_type, filters);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Workflow Document State", {
|
||||
states_remove: function(frm) {
|
||||
frm.trigger('get_orphaned_states_and_count').then(() => {
|
||||
frm.trigger('render_state_table');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class Workflow(Document):
|
|||
|
||||
def update_doc_status(self):
|
||||
'''
|
||||
Checks if the docstatus of a state was updated.
|
||||
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()
|
||||
|
|
@ -112,3 +112,15 @@ class Workflow(Document):
|
|||
def get_fieldnames_for(doctype):
|
||||
return [f.fieldname for f in frappe.get_meta(doctype).fields \
|
||||
if f.fieldname not in no_value_fields]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_workflow_state_count(doctype, workflow_state_field, states):
|
||||
states = frappe.parse_json(states)
|
||||
result = frappe.get_all(
|
||||
doctype,
|
||||
fields=[workflow_state_field, 'count(*) as count', 'docstatus'],
|
||||
filters = {'workflow_state': ['not in', states]},
|
||||
group_by = workflow_state_field
|
||||
)
|
||||
return [r for r in result if r[workflow_state_field]]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue