Merge branch 'develop' of https://github.com/frappe/frappe into paytm-integration

This commit is contained in:
Mangesh-Khairnar 2020-06-03 11:24:01 +05:30
commit eefa54bcaa
69 changed files with 1934 additions and 627 deletions

View file

@ -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();

View file

@ -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'''

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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')

View file

@ -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 = [

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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 \

View file

@ -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)

View file

@ -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) {
// }
});

View file

@ -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
}

View file

@ -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

View file

@ -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) {
// }
});

View file

@ -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
}

View 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

View file

@ -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

View file

@ -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"));
}
});
}
});

View file

@ -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",

View file

@ -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):

View file

@ -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",

View file

@ -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:

View file

@ -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)]

View file

@ -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))

View file

@ -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');

View file

@ -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 &gt; 40000\n</pre>\n"
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total &gt; 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>&lt;h3&gt;Order Overdue&lt;/h3&gt;\n\n&lt;p&gt;Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.&lt;/p&gt;\n\n&lt;!-- show last comment --&gt;\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n&lt;h4&gt;Details&lt;/h4&gt;\n\n&lt;ul&gt;\n&lt;li&gt;Customer: {{ doc.customer }}\n&lt;li&gt;Amount: {{ doc.grand_total }}\n&lt;/ul&gt;\n</pre>"
"options": "<h5>Message Example</h5>\n\n<pre>&lt;h3&gt;Order Overdue&lt;/h3&gt;\n\n&lt;p&gt;Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.&lt;/p&gt;\n\n&lt;!-- show last comment --&gt;\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n&lt;h4&gt;Details&lt;/h4&gt;\n\n&lt;ul&gt;\n&lt;li&gt;Customer: {{ doc.customer }}\n&lt;li&gt;Amount: {{ doc.grand_total }}\n&lt;/ul&gt;\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",

View file

@ -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 [{

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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):
'''

View file

@ -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'
}

View file

@ -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

View 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)

View 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)

View file

@ -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,

View 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;
}
}
}

View file

@ -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)
});
});
}

View file

@ -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()) {

View file

@ -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 ) {

View file

@ -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;

View file

@ -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;
}
};

View file

@ -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);

View file

@ -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 = [{

View file

@ -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 = [
{

View file

@ -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 || [];

View file

@ -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);
}
});
});
},
});

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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)

View file

@ -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]

View file

@ -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'))\

View file

@ -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 = {

View file

@ -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)))

View file

@ -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

View file

@ -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'''

View file

@ -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:

View file

@ -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.")
},
];
];

View file

@ -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);

View file

@ -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');
});
}
});

View file

@ -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]]