diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_time_filters.js similarity index 85% rename from cypress/integration/relative_filters.js rename to cypress/integration/relative_time_filters.js index 411ede62fa..ac70c44345 100644 --- a/cypress/integration/relative_filters.js +++ b/cypress/integration/relative_time_filters.js @@ -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(); diff --git a/frappe/__init__.py b/frappe/__init__.py index f0b6bfe41b..8f36c0c4d3 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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''' diff --git a/frappe/app.py b/frappe/app.py index 3bb764149b..50d09177d6 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -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: diff --git a/frappe/boot.py b/frappe/boot.py index e615cc49fa..8862ce3c61 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -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 diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index 8110f2ec19..42f4440547 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -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) diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py index 6f51c81211..511fac6e0d 100755 --- a/frappe/commands/scheduler.py +++ b/frappe/commands/scheduler.py @@ -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') diff --git a/frappe/commands/site.py b/frappe/commands/site.py index f0c4adb157..399d0efd68 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -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 = [ diff --git a/frappe/commands/translate.py b/frappe/commands/translate.py index 5a48e2b409..48a7fd1db7 100644 --- a/frappe/commands/translate.py +++ b/frappe/commands/translate.py @@ -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 diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 3610393d9a..86db7cdc8f 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -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') diff --git a/frappe/core/doctype/access_log/test_access_log.py b/frappe/core/doctype/access_log/test_access_log.py index 312f77c026..9830507423 100644 --- a/frappe/core/doctype/access_log/test_access_log.py +++ b/frappe/core/doctype/access_log/test_access_log.py @@ -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) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index b35abfa861..a17b3acd02 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -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 diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index c179054550..765ae5fe93 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -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): diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py index dd9cc0706a..f14535cb5f 100644 --- a/frappe/desk/doctype/desk_page/desk_page.py +++ b/frappe/desk/doctype/desk_page/desk_page.py @@ -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 \ diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py index dcfb38bd08..2926a74a55 100644 --- a/frappe/desk/doctype/event/test_event.py +++ b/frappe/desk/doctype/event/test_event.py @@ -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) diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.js b/frappe/desk/doctype/list_view_setting/list_view_setting.js deleted file mode 100644 index 2c70ddf82d..0000000000 --- a/frappe/desk/doctype/list_view_setting/list_view_setting.js +++ /dev/null @@ -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) { - - // } -}); diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.json b/frappe/desk/doctype/list_view_setting/list_view_setting.json deleted file mode 100644 index cd18d3f766..0000000000 --- a/frappe/desk/doctype/list_view_setting/list_view_setting.json +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.py b/frappe/desk/doctype/list_view_setting/list_view_setting.py deleted file mode 100644 index b66dc29a43..0000000000 --- a/frappe/desk/doctype/list_view_setting/list_view_setting.py +++ /dev/null @@ -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 diff --git a/frappe/desk/doctype/list_view_setting/__init__.py b/frappe/desk/doctype/list_view_settings/__init__.py similarity index 100% rename from frappe/desk/doctype/list_view_setting/__init__.py rename to frappe/desk/doctype/list_view_settings/__init__.py diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.js b/frappe/desk/doctype/list_view_settings/list_view_settings.js new file mode 100644 index 0000000000..db33f71675 --- /dev/null +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.js @@ -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) { + + // } +}); \ No newline at end of file diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.json b/frappe/desk/doctype/list_view_settings/list_view_settings.json new file mode 100644 index 0000000000..44761992f1 --- /dev/null +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.json @@ -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 +} \ No newline at end of file diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py new file mode 100644 index 0000000000..74e029f499 --- /dev/null +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py @@ -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 diff --git a/frappe/desk/doctype/list_view_setting/test_list_view_setting.py b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py similarity index 72% rename from frappe/desk/doctype/list_view_setting/test_list_view_setting.py rename to frappe/desk/doctype/list_view_settings/test_list_view_settings.py index 143fc4cce7..c1b2f4a0da 100644 --- a/frappe/desk/doctype/list_view_setting/test_list_view_setting.py +++ b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py @@ -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 diff --git a/frappe/desk/doctype/notification_log/notification_log.js b/frappe/desk/doctype/notification_log/notification_log.js index 654b2b2b06..1f381d115b 100644 --- a/frappe/desk/doctype/notification_log/notification_log.js +++ b/frappe/desk/doctype/notification_log/notification_log.js @@ -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(``); + 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(` +
+
+ + ${attachment.name}.pdf +
+
+ `); + + $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")); + } + }); } }); diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index ecb746df64..050bf85ead 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -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", diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 17eb6371b1..211b3ae5e6 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -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('
', '').replace('
', '') - 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): diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index 6af325507b..85f93e156e 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -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", diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 6b5a13ee27..9b124cd6f4 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -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: diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 8e8102d093..804174b56b 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -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)] diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index e7f56d313e..1bce14fb2d 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -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)) diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 44056955f7..02fc8512ca 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -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'); diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index 14eff2251a..d1526f5fe4 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -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
{{ doc.name }} Delivered
", @@ -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": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\n" + "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\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": "
Message Example
\n\n
<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n
" + "options": "
Message Example
\n\n
<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n
", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.channel=='Slack'", "fieldname": "slack_message_examples", "fieldtype": "HTML", "label": "Message Examples", - "options": "
Message Example
\n\n
*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\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
" + "options": "
Message Example
\n\n
*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\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
", + "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", diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 8c011ade65..8e53b50fa2 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -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 [{ diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 5a1181f31e..1aac339228 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -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 diff --git a/frappe/installer.py b/frappe/installer.py index 54402f0087..4fc19b282a 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -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 diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index 5e464d4882..1d2f7f9495 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -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. diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py index 0b689478d2..887e191e16 100644 --- a/frappe/integrations/frappe_providers/__init__.py +++ b/frappe/integrations/frappe_providers/__init__.py @@ -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)) diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py index 4f33c990f9..3e4b584246 100644 --- a/frappe/integrations/frappe_providers/frappecloud.py +++ b/frappe/integrations/frappe_providers/frappecloud.py @@ -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) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 596aa18b09..19517aa4a1 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 4491a352bc..1e3f127b99 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -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): ''' diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 4384e7c8f5..ea563dfc13 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -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' } diff --git a/frappe/patches.txt b/frappe/patches.txt index 0a7d368ee2..fb5bf447b7 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -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") \ No newline at end of file +execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") +frappe.patches.v13_0.update_date_filters_in_user_settings diff --git a/frappe/patches/v13_0/site_wise_logging.py b/frappe/patches/v13_0/site_wise_logging.py new file mode 100644 index 0000000000..6f04e0c9dd --- /dev/null +++ b/frappe/patches/v13_0/site_wise_logging.py @@ -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) \ No newline at end of file diff --git a/frappe/patches/v13_0/update_date_filters_in_user_settings.py b/frappe/patches/v13_0/update_date_filters_in_user_settings.py new file mode 100644 index 0000000000..d4c6aa1d03 --- /dev/null +++ b/frappe/patches/v13_0/update_date_filters_in_user_settings.py @@ -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) + + diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 15f77fada5..b94257106e 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -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, diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js new file mode 100644 index 0000000000..f2045c9c34 --- /dev/null +++ b/frappe/public/js/frappe/list/list_settings.js @@ -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 += ` +
+ +
+
+ +
+
+ ${me.fields[idx].label} +
+
+ + + +
+
+
`; + } + + fields_html.html(` +
+
+ +
+
+ ${fields} +
+ +
+ `); + + 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; + } + } +} diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index dd9362d664..c282d43d9b 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -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 = $(`
${__('Loading')}...
`) @@ -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) }); }); } diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index 9983a35779..1411b6289d 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -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()) { diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 0f4332a91a..d77481f8b9 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -42,6 +42,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.body = this.$body.get(0); this.$message = $('').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 ) { diff --git a/frappe/public/js/frappe/ui/filters/field_select.js b/frappe/public/js/frappe/ui/filters/field_select.js index 672991a554..65d32184e3 100644 --- a/frappe/public/js/frappe/ui/filters/field_select.js +++ b/frappe/public/js/frappe/ui/filters/field_select.js @@ -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; diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 818612d442..37eab50957 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -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; } }; diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index db6398ca78..ed9ddefe64 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -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); diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index c6bc994a9d..ab20feeedd 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -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: `
${message_html}
` + } + ], + 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 = $(``).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 = [{ diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 2420d6772e..3570420c81 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -153,12 +153,12 @@ frappe.ui.Notifications = class Notifications { let title = target ? `title="${__('Your Target')}"` : ''; let $list_item = !target ? $(`
  • - ${label} + ${__(label)} ${value}
  • `) : $(`
  • - ${label} + ${__(label)}
    @@ -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 = [ { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 24fa946fc4..f4dde5804f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -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 || []; diff --git a/frappe/public/js/frappe/utils/web_page_block.js b/frappe/public/js/frappe/utils/web_page_block.js deleted file mode 100644 index db655df98b..0000000000 --- a/frappe/public/js/frappe/utils/web_page_block.js +++ /dev/null @@ -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); - } - }); - }); - }, -}); \ No newline at end of file diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 1c1049391f..0058310e3f 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -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('
  • %(label)s
  • ', - { module: breadcrumbs.module, label: __(label) })) + { module: breadcrumbs.module, label: __(breadcrumbs.module) })) .appendTo($breadcrumbs); } } diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index c92bdc1b5f..dff4db807e 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -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) { diff --git a/frappe/tests/test_listview.py b/frappe/tests/test_listview.py index 3a73301608..1ef72fdd32 100644 --- a/frappe/tests/test_listview.py +++ b/frappe/tests/test_listview.py @@ -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) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index f2e2319802..60179e98b4 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -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] diff --git a/frappe/utils/bench_helper.py b/frappe/utils/bench_helper.py index 7c5d209179..c46b42b132 100644 --- a/frappe/utils/bench_helper.py +++ b/frappe/utils/bench_helper.py @@ -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'))\ diff --git a/frappe/utils/commands.py b/frappe/utils/commands.py index 99322b50ba..113014c135 100644 --- a/frappe/utils/commands.py +++ b/frappe/utils/commands.py @@ -27,6 +27,15 @@ def add_line_after(function): return empty_line +def add_line_before(function): + """Adds an extra line to STDOUT before the execution of a function this decorates""" + def empty_line(*args, **kwargs): + print() + result = function(*args, **kwargs) + return result + return empty_line + + def log(message, colour=''): """Coloured log outputs to STDOUT""" colours = { diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 7e991f472e..1a4604ffff 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -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))) diff --git a/frappe/utils/error.py b/frappe/utils/error.py index c124410a7f..d0e21a4188 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -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 diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index 5a77434cde..89e3711b0f 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -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''' diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 596595a160..749a41682f 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -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: diff --git a/frappe/website/doctype/web_page/web_page.js b/frappe/website/doctype/web_page/web_page.js index 437a86b5d0..b2e06efc79 100644 --- a/frappe/website/doctype/web_page/web_page.js +++ b/frappe/website/doctype/web_page/web_page.js @@ -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.") }, -]; \ No newline at end of file +]; diff --git a/frappe/website/doctype/website_theme/website_theme.js b/frappe/website/doctype/website_theme/website_theme.js index 28b18a1bcd..75ecbe15e3 100644 --- a/frappe/website/doctype/website_theme/website_theme.js +++ b/frappe/website/doctype/website_theme/website_theme.js @@ -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); diff --git a/frappe/workflow/doctype/workflow/workflow.js b/frappe/workflow/doctype/workflow/workflow.js index 6e12f5fa46..aba6f6fe48 100644 --- a/frappe/workflow/doctype/workflow/workflow.js +++ b/frappe/workflow/doctype/workflow/workflow.js @@ -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 = + `

    + ${__('Are you sure you want to save this document?')} +

    +

    ${__(`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.`)} +

    `; + 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 ` + +
    + ${r[frm.doc.workflow_state_field]} +
    + + ${r.count}`; + }); + }); + + Promise.all(promises).then(rows => { + const rows_html = rows.join(''); + frm.state_table_html = (` + + + + + + + + ${rows_html} + +
    ${__('State')}${__('Count')}
    `); + }); + }, + 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 = $(`
    `).insertAfter(wrapper); + }, + render_state_table: function(frm) { + if (frm.states && frm.states.length) { + const form_state_table_html = + `

    + ${'Document States that do not exist in your Workflow'} +

    + ${frm.state_table_html} +
    `; + 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'); + }); + } +}); diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 62e0b39b08..553f21dbab 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -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]] +