From 6c9adf95a3f81caefea7e3232ff3972fce22ff0e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 7 Oct 2019 16:01:39 +0530 Subject: [PATCH 01/29] fix: rename docname and title --- frappe/model/rename_doc.py | 3 +- frappe/public/js/frappe/form/toolbar.js | 92 ++++++++++++++++++++----- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 12c57f2780..f13290d856 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals, print_function import frappe -from frappe import _ +from frappe import _, bold from frappe.utils import cint from frappe.model.naming import validate_name from frappe.model.dynamic_links import get_dynamic_link_map @@ -83,6 +83,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F frappe.clear_cache() frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype) + frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green') return new diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 7f5882f517..4957845499 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -64,27 +64,83 @@ frappe.ui.form.Toolbar = Class.extend({ can_rename: function() { return this.frm.perm[0].write && this.frm.meta.allow_rename && !this.frm.doc.__islocal; }, - setup_editable_title: function() { - var me = this; - this.page.$title_area.find(".title-text").on("click", function() { - if(me.is_title_editable()) { - frappe.prompt({fieldname: "title", fieldtype:"Data", - label: __("Title"), reqd: 1, "default": me.frm.doc.title }, - function(data) { - if(data.title) { - me.frm.set_value("title", data.title); - if(!me.frm.doc.__islocal) { - me.frm.save_or_update(); - } else { - me.set_title(); - } - } - }, __("Edit Title"), __("Update")); + setup_editable_title: function () { + let me = this; + + this.page.$title_area.find(".title-text").on("click", () => { + let fields = [] + + // check if title is updateable + if (me.is_title_editable()) { + fields.push({ + label: __("New {0}", [__(me.frm.meta.title_field)]), + fieldname: "title", + fieldtype: "Data", + reqd: 1, + default: me.frm.doc.title + }); } - if(me.can_rename()) { - me.frm.rename_doc(); + + // check if docname is updateable + if (me.can_rename()) { + fields.push(... + [{ + label: __("New Docname"), + fieldname: "name", + fieldtype: "Data", + reqd: 1, + default: me.frm.doc.name + }, { + label: __("Merge with existing"), + fieldname: "merge", + fieldtype: "Check", + default: 0 + }] + ); } + + // create dialog + let d = new frappe.ui.Dialog({ + title: __("Rename Fields"), + fields: fields + }); + d.show(); + + d.set_primary_action(__("Rename"), function () { + let args = d.get_values(); + + if (args.title && me.frm.doc[me.frm.meta.title_field] != args.title) { + me.frm.set_value(me.frm.meta.title_field, args.title); + me.frm.save_or_update(); + } + if (args.name && me.frm.doc.name != args.name) { + rename_doc(d, me.frm.doctype, me.frm.doc.name, args); + } + + d.hide(); + }); }); + + function rename_doc(d, doctype, docname, args) { + frappe.call({ + method: "frappe.model.rename_doc.rename_doc", + args: { + doctype: doctype, + old: docname, + new: args.name, + merge: args.merge + }, + btn: d.get_primary_btn(), + callback: function(r,rt) { + if(!r.exc) { + $(document).trigger('rename', [doctype, docname, + r.message || args.new_name]); + if(locals[doctype] && locals[doctype][docname]) + delete locals[doctype][docname]; + } + } + }); + } }, get_dropdown_menu: function(label) { return this.page.add_dropdown(label); From 55d642f07c600ff00873fa0fa8fae7bbf2965a17 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 7 Oct 2019 16:38:10 +0530 Subject: [PATCH 02/29] fix: check for read_only fields in title --- frappe/public/js/frappe/form/toolbar.js | 42 ++++++++++++++----------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 4957845499..0aa27a09ff 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -52,10 +52,14 @@ frappe.ui.form.Toolbar = Class.extend({ this.set_indicator(); }, is_title_editable: function() { - if (this.frm.meta.title_field==="title" + let title_field = this.frm.meta.title_field; + let field = this.frm.get_field(title_field); + if (title_field==="title" && this.frm.perm[0].write && !this.frm.get_docfield("title").options - && !this.frm.doc.__islocal) { + && !this.frm.doc.__islocal + && field + && !field.df.read_only) { return true; } else { return false; @@ -100,25 +104,27 @@ frappe.ui.form.Toolbar = Class.extend({ } // create dialog - let d = new frappe.ui.Dialog({ - title: __("Rename Fields"), - fields: fields - }); - d.show(); + if (fields.length > 0) { + let d = new frappe.ui.Dialog({ + title: __("Rename Fields"), + fields: fields + }); + d.show(); - d.set_primary_action(__("Rename"), function () { - let args = d.get_values(); + d.set_primary_action(__("Rename"), function () { + let args = d.get_values(); - if (args.title && me.frm.doc[me.frm.meta.title_field] != args.title) { - me.frm.set_value(me.frm.meta.title_field, args.title); - me.frm.save_or_update(); - } - if (args.name && me.frm.doc.name != args.name) { - rename_doc(d, me.frm.doctype, me.frm.doc.name, args); - } + if (args.title && me.frm.doc[me.frm.meta.title_field] != args.title) { + me.frm.set_value(me.frm.meta.title_field, args.title); + me.frm.save_or_update(); + } + if (args.name && me.frm.doc.name != args.name) { + rename_doc(d, me.frm.doctype, me.frm.doc.name, args); + } - d.hide(); - }); + d.hide(); + }); + } }); function rename_doc(d, doctype, docname, args) { From 80a4ab48d322593b86bad58a5ab4a628fca07602 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 7 Oct 2019 16:57:19 +0530 Subject: [PATCH 03/29] fix: improved readability variable names in toolbar.js --- frappe/public/js/frappe/form/toolbar.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 0aa27a09ff..ff522ca2e3 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -73,11 +73,12 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.$title_area.find(".title-text").on("click", () => { let fields = [] + let title_field = me.frm.meta.title_field; // check if title is updateable if (me.is_title_editable()) { fields.push({ - label: __("New {0}", [__(me.frm.meta.title_field)]), + label: __("New {0}", [__(title_field)]), fieldname: "title", fieldtype: "Data", reqd: 1, @@ -114,8 +115,8 @@ frappe.ui.form.Toolbar = Class.extend({ d.set_primary_action(__("Rename"), function () { let args = d.get_values(); - if (args.title && me.frm.doc[me.frm.meta.title_field] != args.title) { - me.frm.set_value(me.frm.meta.title_field, args.title); + if (args.title && me.frm.doc[title_field] != args.title) { + me.frm.set_value(title_field, args.title); me.frm.save_or_update(); } if (args.name && me.frm.doc.name != args.name) { From 4f7589d974e108e9c07a000ef51ab9587610a1dd Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 7 Oct 2019 22:30:31 +0530 Subject: [PATCH 04/29] style: fixed indent, semicolon and unused varaibles --- frappe/public/js/frappe/form/toolbar.js | 39 +++++++++++-------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index ff522ca2e3..49cb0f087b 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -72,7 +72,7 @@ frappe.ui.form.Toolbar = Class.extend({ let me = this; this.page.$title_area.find(".title-text").on("click", () => { - let fields = [] + let fields = []; let title_field = me.frm.meta.title_field; // check if title is updateable @@ -88,22 +88,19 @@ frappe.ui.form.Toolbar = Class.extend({ // check if docname is updateable if (me.can_rename()) { - fields.push(... - [{ - label: __("New Docname"), - fieldname: "name", - fieldtype: "Data", - reqd: 1, - default: me.frm.doc.name - }, { - label: __("Merge with existing"), - fieldname: "merge", - fieldtype: "Check", - default: 0 - }] - ); + fields.push(...[{ + label: __("New Docname"), + fieldname: "name", + fieldtype: "Data", + reqd: 1, + default: me.frm.doc.name + }, { + label: __("Merge with existing"), + fieldname: "merge", + fieldtype: "Check", + default: 0 + }]); } - // create dialog if (fields.length > 0) { let d = new frappe.ui.Dialog({ @@ -138,12 +135,10 @@ frappe.ui.form.Toolbar = Class.extend({ merge: args.merge }, btn: d.get_primary_btn(), - callback: function(r,rt) { - if(!r.exc) { - $(document).trigger('rename', [doctype, docname, - r.message || args.new_name]); - if(locals[doctype] && locals[doctype][docname]) - delete locals[doctype][docname]; + callback: function (res) { + if (!res.exc) { + $(document).trigger('rename', [doctype, docname, res.message || args.name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; } } }); From f9577535b7bd825f38c1fed00335efc4403eae49 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 8 Oct 2019 15:55:29 +0530 Subject: [PATCH 05/29] fix: renaming variables, titles --- frappe/public/js/frappe/form/toolbar.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 49cb0f087b..7e1363f739 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -53,13 +53,13 @@ frappe.ui.form.Toolbar = Class.extend({ }, is_title_editable: function() { let title_field = this.frm.meta.title_field; - let field = this.frm.get_field(title_field); - if (title_field==="title" + let field = this.frm.get_docfield(title_field); + + if (title_field && this.frm.perm[0].write && !this.frm.get_docfield("title").options && !this.frm.doc.__islocal - && field - && !field.df.read_only) { + && !field.read_only) { return true; } else { return false; @@ -78,18 +78,18 @@ frappe.ui.form.Toolbar = Class.extend({ // check if title is updateable if (me.is_title_editable()) { fields.push({ - label: __("New {0}", [__(title_field)]), + label: __("New {0}", [__(frappe.model.unscrub(title_field))]), fieldname: "title", fieldtype: "Data", reqd: 1, - default: me.frm.doc.title + default: me.frm.doc[title_field] }); } // check if docname is updateable if (me.can_rename()) { fields.push(...[{ - label: __("New Docname"), + label: __("New Name"), fieldname: "name", fieldtype: "Data", reqd: 1, @@ -104,7 +104,7 @@ frappe.ui.form.Toolbar = Class.extend({ // create dialog if (fields.length > 0) { let d = new frappe.ui.Dialog({ - title: __("Rename Fields"), + title: __("Rename"), fields: fields }); d.show(); From 9f5a71009e1f7e47147b0922e6b5046e5b329dd9 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 8 Oct 2019 16:35:25 +0530 Subject: [PATCH 06/29] fix: blank page on create new form --- frappe/public/js/frappe/form/toolbar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 7e1363f739..53744afb5d 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -54,10 +54,11 @@ frappe.ui.form.Toolbar = Class.extend({ is_title_editable: function() { let title_field = this.frm.meta.title_field; let field = this.frm.get_docfield(title_field); + let title = this.frm.get_docfield("title"); if (title_field && this.frm.perm[0].write - && !this.frm.get_docfield("title").options + && (title ? !title.options : true) && !this.frm.doc.__islocal && !field.read_only) { return true; From 0cf099c5005e040221240afaa16c0ea153f5446a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 9 Oct 2019 12:17:37 +0530 Subject: [PATCH 07/29] fix: change use of title to title_field --- frappe/public/js/frappe/form/toolbar.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 53744afb5d..8c53f042a0 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -53,14 +53,13 @@ frappe.ui.form.Toolbar = Class.extend({ }, is_title_editable: function() { let title_field = this.frm.meta.title_field; - let field = this.frm.get_docfield(title_field); - let title = this.frm.get_docfield("title"); + let doc_field = this.frm.get_docfield(title_field); if (title_field && this.frm.perm[0].write - && (title ? !title.options : true) && !this.frm.doc.__islocal - && !field.read_only) { + && !doc_field.options + && !doc_field.read_only) { return true; } else { return false; From a1c013b226e8dd17da45e462756aea7e2c15ced9 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 9 Oct 2019 12:24:20 +0530 Subject: [PATCH 08/29] fix: label of fieldname picked via meta --- frappe/public/js/frappe/form/toolbar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 8c53f042a0..b3780cb177 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -74,11 +74,12 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.$title_area.find(".title-text").on("click", () => { let fields = []; let title_field = me.frm.meta.title_field; + let title_field_label = me.frm.get_docfield(title_field).label // check if title is updateable if (me.is_title_editable()) { fields.push({ - label: __("New {0}", [__(frappe.model.unscrub(title_field))]), + label: __("New {0}", [__(title_field_label)]), fieldname: "title", fieldtype: "Data", reqd: 1, From 6d6f575904faf3931bb5bca11c7d99c1218d380c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 4 Oct 2019 15:59:15 +0530 Subject: [PATCH 09/29] feat(server scripts): add server scripts! --- frappe/cache_manager.py | 2 +- frappe/core/doctype/server_script/__init__.py | 0 .../doctype/server_script/server_script.js | 8 ++ .../doctype/server_script/server_script.json | 89 ++++++++++++++ .../doctype/server_script/server_script.py | 36 ++++++ .../server_script/server_script_utils.py | 42 +++++++ .../server_script/test_server_script.py | 54 +++++++++ frappe/core/doctype/user/test_user.py | 1 - frappe/handler.py | 5 + frappe/model/document.py | 2 + frappe/utils/jinja.py | 110 +----------------- frappe/utils/safe_globals.py | 93 +++++++++++++++ frappe/website/doctype/web_page/web_page.json | 7 ++ frappe/website/doctype/web_page/web_page.py | 3 +- 14 files changed, 341 insertions(+), 111 deletions(-) create mode 100644 frappe/core/doctype/server_script/__init__.py create mode 100644 frappe/core/doctype/server_script/server_script.js create mode 100644 frappe/core/doctype/server_script/server_script.json create mode 100644 frappe/core/doctype/server_script/server_script.py create mode 100644 frappe/core/doctype/server_script/server_script_utils.py create mode 100644 frappe/core/doctype/server_script/test_server_script.py create mode 100644 frappe/utils/safe_globals.py diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 6c9a59d375..a3b0271c13 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -13,7 +13,7 @@ common_default_keys = ["__default", "__global"] global_cache_keys = ("app_hooks", "installed_apps", "app_modules", "module_app", "notification_config", 'system_settings', 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', - 'active_modules', 'assignment_rule') + 'active_modules', 'assignment_rule', 'server_script_map') user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", diff --git a/frappe/core/doctype/server_script/__init__.py b/frappe/core/doctype/server_script/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js new file mode 100644 index 0000000000..eea8558456 --- /dev/null +++ b/frappe/core/doctype/server_script/server_script.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Server Script', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json new file mode 100644 index 0000000000..ed72eba030 --- /dev/null +++ b/frappe/core/doctype/server_script/server_script.json @@ -0,0 +1,89 @@ +{ + "autoname": "Prompt", + "creation": "2019-09-30 11:56:57.943241", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "script_type", + "reference_doctype", + "doctype_event", + "frequency", + "api_method", + "allow_guest", + "script" + ], + "fields": [ + { + "fieldname": "script_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Script Type", + "options": "DocType Event\nAPI", + "reqd": 1 + }, + { + "fieldname": "script", + "fieldtype": "Code", + "label": "Script", + "reqd": 1 + }, + { + "depends_on": "eval:doc.script_type==='DocType Event'", + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference DocType", + "options": "DocType" + }, + { + "depends_on": "eval:doc.script_type==='DocType Event'", + "fieldname": "doctype_event", + "fieldtype": "Select", + "label": "DocType Event", + "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nBefore Cancel\nBefore Delete" + }, + { + "depends_on": "eval:doc.script_type==='Scheduled Job'", + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Frequency", + "options": "Hourly\nDaily\nWeekly\nMonthly" + }, + { + "depends_on": "eval:doc.script_type==='API'", + "fieldname": "api_method", + "fieldtype": "Data", + "label": "API Method" + }, + { + "default": "0", + "depends_on": "eval:doc.script_type==='API'", + "fieldname": "allow_guest", + "fieldtype": "Check", + "label": "Allow Guest" + } + ], + "modified": "2019-10-04 15:51:57.995877", + "modified_by": "Administrator", + "module": "Core", + "name": "Server Script", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py new file mode 100644 index 0000000000..26650dab99 --- /dev/null +++ b/frappe/core/doctype/server_script/server_script.py @@ -0,0 +1,36 @@ +# -*- 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 +from frappe.utils.safe_globals import get_safe_globals + +class ServerScriptNotEnabled(frappe.PermissionError): pass + +class ServerScript(Document): + def validate(self): + frappe.only_for('System Manager') + + def on_update(self): + frappe.cache().delete_value('server_script_map') + + def execute_method(self): + if not frappe.conf.server_script_enabled: + raise ServerScriptNotEnabled + if self.script_type == 'API': + if frappe.session.user == 'Guest' and not self.allow_guest: + raise frappe.PermissionError + exec(self.script, globals(), None) + else: + raise frappe.DoesNotExistError + + def execute_doc(self, doc): + if not frappe.conf.server_script_enabled: + raise ServerScriptNotEnabled + context = doc.as_dict() + context.doc = doc + exec(self.script, globals(), context) + diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py new file mode 100644 index 0000000000..27894f9b6a --- /dev/null +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -0,0 +1,42 @@ +import frappe + +EVENT_MAP = { + 'before_insert': 'Before Insert', + 'after_insert': 'After Insert', + 'validate': 'Before Save', + 'on_update': 'After Save', + 'before_submit': 'Before Submit', + 'before_cancel': 'Before Cancel', + 'before_delete': 'Before Delete' +} + +def run_server_script_api(method): + script_name = get_server_script_map().get('_api', {}).get(method) + if script_name: + frappe.get_doc('Server Script', script_name).execute_method() + return True + +def run_server_script_for_doc_event(doc, event): + if not event in EVENT_MAP: + return + + if frappe.flags.in_install: + return + + script_name = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) + if script_name: + frappe.get_doc('Server Script', script_name).execute_doc(doc) + +def get_server_script_map(): + script_map = frappe.cache().get_value('server_script_map') + if script_map is None: + script_map = {} + for script in frappe.get_all('Server Script', ('name', 'reference_doctype', 'doctype_event', + 'api_method', 'script_type')): + if script.script_type == 'DocType Event': + script_map.setdefault(script.reference_doctype, {})[script.doctype_event] = script.name + else: + script_map.setdefault('_api', {})[script.api_method] = script.name + frappe.cache().set_value('server_script_map', script_map) + + return script_map diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py new file mode 100644 index 0000000000..1c5e0ddf6d --- /dev/null +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +import requests +from frappe.utils import get_site_url +from frappe.core.doctype.server_script.server_script_utils import get_server_script_map + +class TestServerScript(unittest.TestCase): + def setUp(self): + frappe.cache().delete_value('server_script_map') + + def test_doctype_event(self): + script = get_server_script() + script.script_type = 'DocType Event' + script.script = 'frappe.flags._ping = True' + script.reference_doctype = 'ToDo' + script.doctype_event = 'Before Save' + script.save() + frappe.db.commit() + + frappe.flags._ping = False + frappe.get_doc(dict(doctype='ToDo', description='test todo')).insert() + self.assertTrue(frappe.flags._ping) + + def test_api(self): + script = get_server_script() + script.script_type = 'API' + script.api_method = 'test_server_script' + script.allow_guest = 1 + script.script = 'frappe.response["message"] = "hello"' + script.save() + frappe.db.commit() + + response = requests.post(get_site_url(frappe.local.site) + "/api/method/test_server_script") + self.assertEqual(response.status_code, 200) + self.assertEqual("hello", response.json()["message"]) + + +def get_server_script(): + if frappe.db.exists('Server Script', 'Test Server Script'): + return frappe.get_doc('Server Script', 'Test Server Script') + else: + script = frappe.get_doc(dict( + doctype = 'Server Script', + name = 'Test Server Script', + script = '# nothing', + script_type = 'DocType Event' + )).insert() + + return script diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index b298b8388e..0d981c9e9e 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import frappe, unittest -import requests from frappe.model.delete_doc import delete_doc from frappe.utils.data import today, add_to_date diff --git a/frappe/handler.py b/frappe/handler.py index fa570f334d..ac6f4f4ecb 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -9,6 +9,7 @@ import frappe.sessions import frappe.desk.form.run_method from frappe.utils.response import build_response from frappe.utils import cint +from frappe.core.doctype.server_script.server_script_utils import run_server_script_api from werkzeug.wrappers import Response from six import string_types @@ -38,6 +39,10 @@ def execute_cmd(cmd, from_async=False): cmd = hook break + # via server script + if run_server_script_api(cmd): + return None + try: method = get_attr(cmd) except Exception as e: diff --git a/frappe/model/document.py b/frappe/model/document.py index f93c366ffb..7f04895308 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -17,6 +17,7 @@ from frappe.model.workflow import validate_workflow from frappe.utils.global_search import update_global_search from frappe.integrations.doctype.webhook import run_webhooks from frappe.desk.form.document_follow import follow_document +from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event # once_only validation # methods @@ -787,6 +788,7 @@ class Document(BaseDocument): self.run_notifications(method) run_webhooks(self, method) + run_server_script_for_doc_event(self, method) return out diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index 28e3b3d463..d4c8ddc50e 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals def get_jenv(): import frappe + from frappe.utils.safe_globals import get_safe_globals if not getattr(frappe.local, 'jenv', None): from jinja2 import DebugUndefined @@ -14,7 +15,7 @@ def get_jenv(): undefined=DebugUndefined) set_filters(jenv) - jenv.globals.update(get_allowed_functions_for_jenv()) + jenv.globals.update(get_safe_globals()) frappe.local.jenv = jenv @@ -80,98 +81,6 @@ def render_template(template, context, is_path=None, safe_render=True): throw(title="Jinja Template Error", msg="
{template}
{tb}
".format(template=template, tb=get_traceback())) -def get_allowed_functions_for_jenv(): - import os, json - import frappe - import frappe.utils - import frappe.utils.data - from frappe.model.document import get_controller - from frappe.website.utils import (get_shade, get_toc, get_next_link) - from frappe.modules import scrub - import mimetypes - from html2text import html2text - from frappe.www.printview import get_visible_columns - - datautils = {} - if frappe.db: - date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd" - else: - date_format = 'yyyy-mm-dd' - - for key, obj in frappe.utils.data.__dict__.items(): - if key.startswith("_"): - # ignore - continue - - if hasattr(obj, "__call__"): - # only allow functions - datautils[key] = obj - - if "_" in getattr(frappe.local, 'form_dict', {}): - del frappe.local.form_dict["_"] - - user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" - - out = { - # make available limited methods of frappe - "frappe": { - "_": frappe._, - "get_url": frappe.utils.get_url, - 'format': frappe.format_value, - "format_value": frappe.format_value, - 'date_format': date_format, - "format_date": frappe.utils.data.global_date_format, - "form_dict": getattr(frappe.local, 'form_dict', {}), - "get_hooks": frappe.get_hooks, - "get_meta": frappe.get_meta, - "get_doc": frappe.get_doc, - "get_cached_doc": frappe.get_cached_doc, - "get_list": frappe.get_list, - "get_all": frappe.get_all, - 'get_system_settings': frappe.get_system_settings, - "utils": datautils, - "user": user, - "get_fullname": frappe.utils.get_fullname, - "get_gravatar": frappe.utils.get_gravatar_url, - "full_name": frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest", - "render_template": frappe.render_template, - "request": getattr(frappe.local, 'request', {}), - 'session': { - 'user': user, - 'csrf_token': frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' - }, - "socketio_port": frappe.conf.socketio_port, - }, - 'style': { - 'border_color': '#d1d8dd' - }, - 'get_toc': get_toc, - 'get_next_link': get_next_link, - "_": frappe._, - "get_shade": get_shade, - "scrub": scrub, - "guess_mimetype": mimetypes.guess_type, - 'html2text': html2text, - 'json': json, - "dev_server": 1 if os.environ.get('DEV_SERVER', False) else 0 - } - - if not frappe.flags.in_setup_help: - out['get_visible_columns'] = get_visible_columns - out['frappe']['date_format'] = date_format - out['frappe']["db"] = { - "get_value": frappe.db.get_value, - "get_single_value": frappe.db.get_single_value, - "get_default": frappe.db.get_default, - "escape": frappe.db.escape, - } - - # load jenv methods from hooks.py - for method_name, method_definition in get_jenv_customization("methods"): - out[method_name] = frappe.get_attr(method_definition) - - return out - def get_jloader(): import frappe if not getattr(frappe.local, 'jloader', None): @@ -216,18 +125,3 @@ def set_filters(jenv): jenv.filters["abs_url"] = abs_url if frappe.flags.in_setup_help: return - - # load jenv_filters from hooks.py - for filter_name, filter_function in get_jenv_customization("filters"): - jenv.filters[filter_name] = frappe.get_attr(filter_function) - -def get_jenv_customization(customizable_type): - import frappe - - if getattr(frappe.local, "site", None): - for app in frappe.get_installed_apps(): - for jenv_customizable, jenv_customizable_definition in frappe.get_hooks(app_name=app).get("jenv", {}).items(): - if customizable_type == jenv_customizable: - for data in jenv_customizable_definition: - split_data = data.split(":") - yield split_data[0], split_data[1] diff --git a/frappe/utils/safe_globals.py b/frappe/utils/safe_globals.py new file mode 100644 index 0000000000..61a3649f93 --- /dev/null +++ b/frappe/utils/safe_globals.py @@ -0,0 +1,93 @@ + +from RestrictedPython import safe_globals +import os, json +import mimetypes +from html2text import html2text + +def get_safe_globals(): + import frappe + import frappe.utils + import frappe.utils.data + from frappe.model.document import get_controller + from frappe.website.utils import (get_shade, get_toc, get_next_link) + from frappe.modules import scrub + from frappe.www.printview import get_visible_columns + + datautils = {} + if frappe.db: + date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd" + else: + date_format = 'yyyy-mm-dd' + + for key, obj in frappe.utils.data.__dict__.items(): + if key.startswith("_"): + # ignore + continue + + if hasattr(obj, "__call__"): + # only allow functions + datautils[key] = obj + + if "_" in getattr(frappe.local, 'form_dict', {}): + del frappe.local.form_dict["_"] + + user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" + + out = frappe._dict( + # make available limited methods of frappe + frappe = frappe._dict({ + "_": frappe._, + 'flags': frappe.flags, + "get_url": frappe.utils.get_url, + 'format': frappe.format_value, + "format_value": frappe.format_value, + 'date_format': date_format, + "format_date": frappe.utils.data.global_date_format, + "form_dict": getattr(frappe.local, 'form_dict', {}), + "get_hooks": frappe.get_hooks, + "get_meta": frappe.get_meta, + "get_doc": frappe.get_doc, + "get_cached_doc": frappe.get_cached_doc, + "get_list": frappe.get_list, + "get_all": frappe.get_all, + 'get_system_settings': frappe.get_system_settings, + "utils": datautils, + "user": user, + "get_fullname": frappe.utils.get_fullname, + "get_gravatar": frappe.utils.get_gravatar_url, + "full_name": frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest", + "render_template": frappe.render_template, + "request": getattr(frappe.local, 'request', {}), + 'session': frappe._dict( + user = user, + csrf_token = frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' + ), + "socketio_port": frappe.conf.socketio_port, + }), + style = frappe._dict( + border_color = '#d1d8dd' + ), + get_toc = get_toc, + get_next_link = get_next_link, + _ = frappe._, + get_shade = get_shade, + scrub = scrub, + guess_mimetype = mimetypes.guess_type, + html2text = html2text, + json = json, + dev_server = 1 if os.environ.get('DEV_SERVER', False) else 0 + ) + + if not frappe.flags.in_setup_help: + out.get_visible_columns = get_visible_columns + out.frappe.date_format = date_format + out.frappe.db = frappe._dict( + get_value = frappe.db.get_value, + get_single_value = frappe.db.get_single_value, + get_default = frappe.db.get_default, + escape = frappe.db.escape, + ) + + out.update(safe_globals) + + return out diff --git a/frappe/website/doctype/web_page/web_page.json b/frappe/website/doctype/web_page/web_page.json index 3dd32a65e9..645d83e155 100644 --- a/frappe/website/doctype/web_page/web_page.json +++ b/frappe/website/doctype/web_page/web_page.json @@ -18,6 +18,7 @@ "end_date", "sb1", "content_type", + "dynamic_template", "main_section", "main_section_md", "main_section_html", @@ -235,6 +236,12 @@ "fieldname": "set_meta_tags", "fieldtype": "Button", "label": "Set Meta Tags" + }, + { + "default": "0", + "fieldname": "dynamic_template", + "fieldtype": "Check", + "label": "Dynamic Template" } ], "has_web_view": 1, diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index c0c69e888d..9509e57798 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -30,6 +30,7 @@ class WebPage(WebsiteGenerator): def get_context(self, context): context.main_section = get_html_content_based_on_type(self, 'main_section', self.content_type) + self.render_dynamic(context) # if static page, get static content if context.slideshow: @@ -57,7 +58,7 @@ class WebPage(WebsiteGenerator): def render_dynamic(self, context): # dynamic - is_jinja = "" in context.main_section + is_jinja = context.dynamic_template or "" in context.main_section if is_jinja or ("{{" in context.main_section): try: context["main_section"] = render_template(context.main_section, From 6c4253e0af11a11458627aac9b85504a7a98c134 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 4 Oct 2019 16:09:34 +0530 Subject: [PATCH 10/29] fix(minor): remove RestrictedPython --- frappe/utils/safe_globals.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/frappe/utils/safe_globals.py b/frappe/utils/safe_globals.py index 61a3649f93..0decf110f4 100644 --- a/frappe/utils/safe_globals.py +++ b/frappe/utils/safe_globals.py @@ -1,5 +1,4 @@ -from RestrictedPython import safe_globals import os, json import mimetypes from html2text import html2text @@ -88,6 +87,4 @@ def get_safe_globals(): escape = frappe.db.escape, ) - out.update(safe_globals) - return out From ee417766a2b9b5a0a2aa702c6c996eb3229fc8b8 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 7 Oct 2019 12:01:58 +0530 Subject: [PATCH 11/29] fix(minor): server script enabled --- frappe/core/doctype/server_script/test_server_script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 1c5e0ddf6d..1380c7899f 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -11,6 +11,7 @@ from frappe.core.doctype.server_script.server_script_utils import get_server_scr class TestServerScript(unittest.TestCase): def setUp(self): + frappe.conf.server_script_enabled = True frappe.cache().delete_value('server_script_map') def test_doctype_event(self): From f6f2ea89ce203cfbd34a96bbbf35c27d857bee31 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 9 Oct 2019 13:01:09 +0530 Subject: [PATCH 12/29] fix(minor): server script enabled --- .travis/mariadb.json | 3 ++- .travis/postgres.json | 3 ++- frappe/core/doctype/server_script/test_server_script.py | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis/mariadb.json b/.travis/mariadb.json index 550ad94769..c7e7f598e2 100644 --- a/.travis/mariadb.json +++ b/.travis/mariadb.json @@ -10,5 +10,6 @@ "admin_password": "admin", "root_login": "root", "root_password": "travis", - "host_name": "http://test_site:8000" + "host_name": "http://test_site:8000", + "server_script_enabled": true } diff --git a/.travis/postgres.json b/.travis/postgres.json index 619dd91f10..169a025041 100644 --- a/.travis/postgres.json +++ b/.travis/postgres.json @@ -10,5 +10,6 @@ "admin_password": "admin", "root_login": "postgres", "root_password": "travis", - "host_name": "http://test_site:8000" + "host_name": "http://test_site:8000", + "server_script_enabled": true } diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 1380c7899f..1c5e0ddf6d 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -11,7 +11,6 @@ from frappe.core.doctype.server_script.server_script_utils import get_server_scr class TestServerScript(unittest.TestCase): def setUp(self): - frappe.conf.server_script_enabled = True frappe.cache().delete_value('server_script_map') def test_doctype_event(self): From 6108c2ce33ff43f97c44fb6c9763dcb331973370 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 9 Oct 2019 14:02:25 +0530 Subject: [PATCH 13/29] fix(minor): test_exporter_new.py --- frappe/core/doctype/data_import/test_exporter_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/data_import/test_exporter_new.py b/frappe/core/doctype/data_import/test_exporter_new.py index dee5af543c..ccee0472d5 100644 --- a/frappe/core/doctype/data_import/test_exporter_new.py +++ b/frappe/core/doctype/data_import/test_exporter_new.py @@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase): e = Exporter('Web Page', export_fields='All') csv_array = e.get_csv_array() header = csv_array[0] - self.assertEqual(len(header), 23) + self.assertEqual(len(header), 24) def test_exports_selected_fields(self): From 9d615f7f12c7eebf1d5eb9f1c41d95cfdc02c37a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 09:45:36 +0530 Subject: [PATCH 14/29] fix(security): use restricted python --- frappe/core/doctype/report/report.json | 979 ++++-------------- frappe/core/doctype/report/report.py | 42 +- frappe/core/doctype/report/test_report.py | 3 + .../doctype/server_script/server_script.json | 33 +- .../doctype/server_script/server_script.py | 11 +- .../server_script/server_script_utils.py | 14 +- .../server_script/test_server_script.py | 85 +- frappe/desk/query_report.py | 43 +- frappe/tests/test_safe_exec.py | 11 + frappe/utils/jinja.py | 2 +- .../utils/{safe_globals.py => safe_exec.py} | 37 +- requirements.txt | 1 + 12 files changed, 383 insertions(+), 878 deletions(-) create mode 100644 frappe/tests/test_safe_exec.py rename frappe/utils/{safe_globals.py => safe_exec.py} (76%) diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index 339b6c5d4c..40d2417a56 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -1,777 +1,204 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:report_name", - "beta": 0, - "creation": "2013-03-09 15:45:57", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "report_name", - "fieldtype": "Data", - "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": "Report Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 1 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "ref_doctype", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Ref DocType", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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, - "fetch_if_empty": 0, - "fieldname": "reference_report", - "fieldtype": "Data", - "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": "Reference Report", - "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, - "fetch_if_empty": 0, - "fieldname": "is_standard", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Is Standard", - "length": 0, - "no_copy": 0, - "options": "No\nYes", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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, - "fetch_if_empty": 0, - "fieldname": "module", - "fieldtype": "Link", - "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": "Module", - "length": 0, - "no_copy": 0, - "options": "Module Def", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "add_total_row", - "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": "Add Total Row", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "report_type", - "fieldtype": "Select", - "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": "Report Type", - "length": 0, - "no_copy": 0, - "options": "Report Builder\nQuery Report\nScript Report\nCustom Report", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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, - "fetch_if_empty": 0, - "fieldname": "disabled", - "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": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "icon", - "fieldtype": "Data", - "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": "Icon", - "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, - "fetch_if_empty": 0, - "fieldname": "color", - "fieldtype": "Data", - "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": "Color", - "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, - "depends_on": "eval: doc.is_standard == \"No\"", - "fetch_if_empty": 0, - "fieldname": "letter_head", - "fieldtype": "Link", - "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": "Letter Head", - "length": 0, - "no_copy": 0, - "options": "Letter Head", - "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, - "fetch_if_empty": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "depends_on": "eval:doc.report_type==\"Query Report\"", - "fetch_if_empty": 0, - "fieldname": "query", - "fieldtype": "Code", - "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": "Query", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "depends_on": "", - "description": "JavaScript Format: frappe.query_reports['REPORTNAME'] = {}", - "fetch_if_empty": 0, - "fieldname": "javascript", - "fieldtype": "Code", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Javascript", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "depends_on": "eval:doc.report_type==\"Report Builder\" || \"Custom Report\"", - "fetch_if_empty": 0, - "fieldname": "json", - "fieldtype": "Code", - "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": "JSON", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "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, - "fetch_if_empty": 0, - "fieldname": "permission_rules", - "fieldtype": "Section Break", - "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": "", - "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, - "depends_on": "eval:doc.is_standard == 'Yes'", - "fetch_if_empty": 0, - "fieldname": "roles", - "fieldtype": "Table", - "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": "Roles", - "length": 0, - "no_copy": 0, - "options": "Has Role", - "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, - "fetch_if_empty": 0, - "fieldname": "disable_prepared_report", - "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 Prepared Report", - "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, - "fetch_if_empty": 0, - "fieldname": "prepared_report", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Prepared Report", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "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_toolbar": 0, - "icon": "", - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-12 15:53:14.194591", - "modified_by": "Administrator", - "module": "Core", - "name": "Report", - "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": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Report Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 - } \ No newline at end of file + "autoname": "field:report_name", + "creation": "2013-03-09 15:45:57", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "report_name", + "ref_doctype", + "reference_report", + "is_standard", + "module", + "column_break_4", + "report_type", + "letter_head", + "add_total_row", + "disabled", + "disable_prepared_report", + "prepared_report", + "section_break_6", + "query", + "javascript", + "report_script", + "json", + "permission_rules", + "roles" + ], + "fields": [ + { + "fieldname": "report_name", + "fieldtype": "Data", + "label": "Report Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Ref DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "reference_report", + "fieldtype": "Data", + "label": "Reference Report" + }, + { + "fieldname": "is_standard", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Is Standard", + "options": "No\nYes", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "add_total_row", + "fieldtype": "Check", + "label": "Add Total Row" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "report_type", + "fieldtype": "Select", + "label": "Report Type", + "options": "Report Builder\nQuery Report\nScript Report\nCustom Report", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "eval: doc.is_standard == \"No\"", + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.report_type==\"Query Report\"", + "fieldname": "query", + "fieldtype": "Code", + "label": "Query" + }, + { + "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"", + "description": "JavaScript Format: frappe.query_reports['REPORTNAME'] = {}", + "fieldname": "javascript", + "fieldtype": "Code", + "label": "Javascript" + }, + { + "depends_on": "eval:doc.report_type==\"Report Builder\" || \"Custom Report\"", + "fieldname": "json", + "fieldtype": "Code", + "label": "JSON", + "read_only": 1 + }, + { + "fieldname": "permission_rules", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.is_standard == 'Yes'", + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" + }, + { + "default": "0", + "fieldname": "disable_prepared_report", + "fieldtype": "Check", + "label": "Disable Prepared Report" + }, + { + "default": "0", + "fieldname": "prepared_report", + "fieldtype": "Check", + "hidden": 1, + "label": "Prepared Report", + "read_only": 1 + }, + { + "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"", + "description": "output in the form of `data = [columns, result]`", + "fieldname": "report_script", + "fieldtype": "Code", + "label": "Script" + } + ], + "idx": 1, + "modified": "2019-10-09 15:43:08.577610", + "modified_by": "Administrator", + "module": "Core", + "name": "Report", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Report Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All" + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index df62f94bcb..6e07c1a650 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -import json +import json, datetime from frappe import _ import frappe.desk.query_report from frappe.utils import cint @@ -27,16 +27,15 @@ class Report(Document): if frappe.session.user=="Administrator" and getattr(frappe.local.conf, 'developer_mode',0)==1: self.is_standard = "Yes" - if self.is_standard == "No" and frappe.db.get_value("Report", self.name, "is_standard") == "Yes": - frappe.throw(_("Cannot edit a standard report. Please duplicate and create a new report")) + if self.is_standard == "No": + frappe.only_for('Script Manager') + + if frappe.db.get_value("Report", self.name, "is_standard") == "Yes": + frappe.throw(_("Cannot edit a standard report. Please duplicate and create a new report")) if self.is_standard == "Yes" and frappe.session.user!="Administrator": frappe.throw(_("Only Administrator can save a standard report. Please rename and save.")) - if self.report_type in ("Query Report", "Script Report") \ - and frappe.session.user!="Administrator": - frappe.throw(_("Only Administrator allowed to create Query / Script Reports")) - if self.report_type == "Report Builder": self.update_report_json() @@ -92,6 +91,35 @@ class Report(Document): make_boilerplate("controller.py", self, {"name": self.name}) make_boilerplate("controller.js", self, {"name": self.name}) + def execute_script_report(self, filters): + threshold = 30 + res = [] + + start_time = datetime.datetime.now() + # The JOB + if self.is_standard == 'Yes': + module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module") + method_name = get_report_module_dotted_path(module, report.name) + ".execute" + res = frappe.get_attr(method_name)(frappe._dict(filters)) + else: + if not frappe.conf.server_script_enabled: + raise ServerScriptNotEnabled + loc = {"filters": frappe._dict(filters), 'data':[]} + exec(self.report_script, globals(), loc) + res = loc['data'] + + end_time = datetime.datetime.now() + + execution_time = (end_time - start_time).seconds + + if execution_time > threshold and not self.prepared_report: + self.db_set('prepared_report', 1) + + frappe.cache().hset('report_execution_time', self.name, execution_time) + + return res + + def get_data(self, filters=None, limit=None, user=None, as_dict=False): columns = [] out = [] diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 258734743f..ad21675ee5 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -68,3 +68,6 @@ class TestReport(unittest.TestCase): self.assertEqual(columns[1].get('label'), 'User Type') self.assertTrue('Administrator' in [d[0] for d in data]) frappe.delete_doc('Report', 'User Activity Report Without Sort') + + def test_non_standard_script_report(self): + pass diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index ed72eba030..82fff31394 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -6,11 +6,13 @@ "engine": "InnoDB", "field_order": [ "script_type", + "disabled", + "column_break_3", "reference_doctype", "doctype_event", - "frequency", "api_method", "allow_guest", + "section_break_8", "script" ], "fields": [ @@ -33,7 +35,7 @@ "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, - "label": "Reference DocType", + "label": "Reference Document Type", "options": "DocType" }, { @@ -41,14 +43,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nBefore Cancel\nBefore Delete" - }, - { - "depends_on": "eval:doc.script_type==='Scheduled Job'", - "fieldname": "frequency", - "fieldtype": "Select", - "label": "Frequency", - "options": "Hourly\nDaily\nWeekly\nMonthly" + "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete" }, { "depends_on": "eval:doc.script_type==='API'", @@ -62,9 +57,23 @@ "fieldname": "allow_guest", "fieldtype": "Check", "label": "Allow Guest" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" } ], - "modified": "2019-10-04 15:51:57.995877", + "modified": "2019-10-09 15:08:40.085059", "modified_by": "Administrator", "module": "Core", "name": "Server Script", @@ -78,7 +87,7 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Script Manager", "share": 1, "write": 1 } diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 26650dab99..50d64b2180 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -6,13 +6,13 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe.utils.safe_globals import get_safe_globals +from frappe.utils.safe_exec import safe_exec class ServerScriptNotEnabled(frappe.PermissionError): pass class ServerScript(Document): def validate(self): - frappe.only_for('System Manager') + frappe.only_for('Script Manager') def on_update(self): frappe.cache().delete_value('server_script_map') @@ -23,14 +23,13 @@ class ServerScript(Document): if self.script_type == 'API': if frappe.session.user == 'Guest' and not self.allow_guest: raise frappe.PermissionError - exec(self.script, globals(), None) + safe_exec(self.script) else: raise frappe.DoesNotExistError def execute_doc(self, doc): if not frappe.conf.server_script_enabled: raise ServerScriptNotEnabled - context = doc.as_dict() - context.doc = doc - exec(self.script, globals(), context) + context = dict(doc = doc) + safe_exec(self.script, None, context) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 27894f9b6a..2bece465dd 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -6,8 +6,11 @@ EVENT_MAP = { 'validate': 'Before Save', 'on_update': 'After Save', 'before_submit': 'Before Submit', + 'on_submit': 'After Submit', 'before_cancel': 'Before Cancel', - 'before_delete': 'Before Delete' + 'on_cancel': 'After Cancel', + 'on_trash': 'Before Delete', + 'after_delete': 'After Delete', } def run_server_script_api(method): @@ -23,9 +26,10 @@ def run_server_script_for_doc_event(doc, event): if frappe.flags.in_install: return - script_name = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) - if script_name: - frappe.get_doc('Server Script', script_name).execute_doc(doc) + scripts = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) + if scripts: + for script_name in scripts: + frappe.get_doc('Server Script', script_name).execute_doc(doc) def get_server_script_map(): script_map = frappe.cache().get_value('server_script_map') @@ -34,7 +38,7 @@ def get_server_script_map(): for script in frappe.get_all('Server Script', ('name', 'reference_doctype', 'doctype_event', 'api_method', 'script_type')): if script.script_type == 'DocType Event': - script_map.setdefault(script.reference_doctype, {})[script.doctype_event] = script.name + script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: script_map.setdefault('_api', {})[script.api_method] = script.name frappe.cache().set_value('server_script_map', script_map) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 1c5e0ddf6d..287b18d7fa 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -9,46 +9,67 @@ import requests from frappe.utils import get_site_url from frappe.core.doctype.server_script.server_script_utils import get_server_script_map +scripts = [ + dict( + name='test_todo', + script_type = 'DocType Event', + doctype_event = 'Before Insert', + reference_doctype = 'ToDo', + script = ''' +if "test" in doc.description: + doc.status = 'Closed' +''' + ), + dict( + name='test_todo_validate', + script_type = 'DocType Event', + doctype_event = 'Before Insert', + reference_doctype = 'ToDo', + script = ''' +if "validate" in doc.description: + raise frappe.ValidationError +''' + ), + dict( + name='test_api', + script_type = 'API', + api_method = 'test_server_script', + allow_guest = 1, + script = ''' +frappe.response['message'] = 'hello' +''' + ) +] class TestServerScript(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.commit() + frappe.db.sql('truncate `tabServer Script`') + frappe.get_doc('User', 'Administrator').add_roles('Script Manager') + for script in scripts: + script_doc = frappe.get_doc(doctype ='Server Script') + script_doc.update(script) + script_doc.insert() + + frappe.db.commit() + + # @classmethod + # def tearDownClass(cls): + # frappe.db.sql('truncate `tabServer Script`') + def setUp(self): frappe.cache().delete_value('server_script_map') def test_doctype_event(self): - script = get_server_script() - script.script_type = 'DocType Event' - script.script = 'frappe.flags._ping = True' - script.reference_doctype = 'ToDo' - script.doctype_event = 'Before Save' - script.save() - frappe.db.commit() + todo = frappe.get_doc(dict(doctype='ToDo', description='hello')).insert() + self.assertEqual(todo.status, 'Open') - frappe.flags._ping = False - frappe.get_doc(dict(doctype='ToDo', description='test todo')).insert() - self.assertTrue(frappe.flags._ping) + todo = frappe.get_doc(dict(doctype='ToDo', description='test todo')).insert() + self.assertEqual(todo.status, 'Closed') + + self.assertRaises(frappe.ValidationError, frappe.get_doc(dict(doctype='ToDo', description='validate me')).insert) def test_api(self): - script = get_server_script() - script.script_type = 'API' - script.api_method = 'test_server_script' - script.allow_guest = 1 - script.script = 'frappe.response["message"] = "hello"' - script.save() - frappe.db.commit() - response = requests.post(get_site_url(frappe.local.site) + "/api/method/test_server_script") self.assertEqual(response.status_code, 200) self.assertEqual("hello", response.json()["message"]) - - -def get_server_script(): - if frappe.db.exists('Server Script', 'Test Server Script'): - return frappe.get_doc('Server Script', 'Test Server Script') - else: - script = frappe.get_doc(dict( - doctype = 'Server Script', - name = 'Test Server Script', - script = '# nothing', - script_type = 'DocType Event' - )).insert() - - return script diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 5d1829abb2..192290c127 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -import os, json, datetime +import os, json from frappe import _ from frappe.modules import scrub, get_module_path @@ -63,38 +63,21 @@ def generate_report_result(report, filters=None, user=None): result = [list(t) for t in frappe.db.sql(report.query, filters)] columns = [cstr(c[0]) for c in frappe.db.get_description()] - else: - module = report.module or frappe.db.get_value("DocType", report.ref_doctype, "module") - if report.is_standard == "Yes": - method_name = get_report_module_dotted_path(module, report.name) + ".execute" - threshold = 30 - res = [] - start_time = datetime.datetime.now() - # The JOB - res = frappe.get_attr(method_name)(frappe._dict(filters)) + elif report.report_type == 'Script Report': + res = report.execute_script_report(filters) - end_time = datetime.datetime.now() + columns, result = res[0], res[1] + if len(res) > 2: + message = res[2] + if len(res) > 3: + chart = res[3] + if len(res) > 4: + data_to_be_printed = res[4] - execution_time = (end_time - start_time).seconds - - if execution_time > threshold and not report.prepared_report: - report.db_set('prepared_report', 1) - - frappe.cache().hset('report_execution_time', report.name, execution_time) - - columns, result = res[0], res[1] - if len(res) > 2: - message = res[2] - if len(res) > 3: - chart = res[3] - if len(res) > 4: - data_to_be_printed = res[4] - - - if report.custom_columns: - columns = json.loads(report.custom_columns) - result = add_data_to_custom_columns(columns, result) + if report.custom_columns: + columns = json.loads(report.custom_columns) + result = add_data_to_custom_columns(columns, result) if result: result = get_filtered_data(report.ref_doctype, columns, result, user) diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py new file mode 100644 index 0000000000..b868b4fcb8 --- /dev/null +++ b/frappe/tests/test_safe_exec.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +import unittest +import frappe +from frappe.utils.safe_exec import safe_exec + +class TestSafeExec(unittest.TestCase): + def test_import_fails(self): + self.assertRaises(ImportError, safe_exec, 'import os') + + def test_internal_attributes(self): + self.assertRaises(SyntaxError, safe_exec, '().__class__.__call__') \ No newline at end of file diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index d4c8ddc50e..befb9336fa 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals def get_jenv(): import frappe - from frappe.utils.safe_globals import get_safe_globals + from frappe.utils.safe_exec import get_safe_globals if not getattr(frappe.local, 'jenv', None): from jinja2 import DebugUndefined diff --git a/frappe/utils/safe_globals.py b/frappe/utils/safe_exec.py similarity index 76% rename from frappe/utils/safe_globals.py rename to frappe/utils/safe_exec.py index 0decf110f4..89acd14d2f 100644 --- a/frappe/utils/safe_globals.py +++ b/frappe/utils/safe_exec.py @@ -1,7 +1,12 @@ -import os, json +import os, json, inspect import mimetypes from html2text import html2text +from RestrictedPython import compile_restricted, safe_globals + +def safe_exec(script, _globals=None, _locals=None): + if not _globals: _globals = get_safe_globals() + exec(compile_restricted(script), _globals, _locals) def get_safe_globals(): import frappe @@ -11,6 +16,7 @@ def get_safe_globals(): from frappe.website.utils import (get_shade, get_toc, get_next_link) from frappe.modules import scrub from frappe.www.printview import get_visible_columns + import frappe.exceptions datautils = {} if frappe.db: @@ -18,14 +24,7 @@ def get_safe_globals(): else: date_format = 'yyyy-mm-dd' - for key, obj in frappe.utils.data.__dict__.items(): - if key.startswith("_"): - # ignore - continue - - if hasattr(obj, "__call__"): - # only allow functions - datautils[key] = obj + add_module_properties(frappe.utils.data, datautils, lambda obj: hasattr(obj, "__call__")) if "_" in getattr(frappe.local, 'form_dict', {}): del frappe.local.form_dict["_"] @@ -77,6 +76,8 @@ def get_safe_globals(): dev_server = 1 if os.environ.get('DEV_SERVER', False) else 0 ) + add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)) + if not frappe.flags.in_setup_help: out.get_visible_columns = get_visible_columns out.frappe.date_format = date_format @@ -87,4 +88,22 @@ def get_safe_globals(): escape = frappe.db.escape, ) + if frappe.response: + out.frappe.response = frappe.response + + out.update(safe_globals) + + # default writer allows write access + out._write_ = lambda obj: obj + return out + +def add_module_properties(module, data, filter_method): + for key, obj in module.__dict__.items(): + if key.startswith("_"): + # ignore + continue + + if filter_method(obj): + # only allow functions + data[key] = obj diff --git a/requirements.txt b/requirements.txt index 5e56468c0e..84788c863e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -65,3 +65,4 @@ Pygments==2.2.0 frontmatter PyYAML==3.13 xlrd +RestrictedPython==5.0 \ No newline at end of file From 9d38990c2d30af50d674342ffb45ee9fef272351 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 13:04:06 +0530 Subject: [PATCH 15/29] fix(report): custom script report --- frappe/core/doctype/report/report.py | 15 +++-- frappe/core/doctype/report/test_report.py | 33 +++++++++- frappe/desk/query_report.py | 5 -- frappe/utils/safe_exec.py | 75 ++++++++++++++--------- 4 files changed, 86 insertions(+), 42 deletions(-) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 6e07c1a650..830dfcd895 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe import json, datetime -from frappe import _ +from frappe import _, scrub import frappe.desk.query_report from frappe.utils import cint from frappe.model.document import Document @@ -14,6 +14,7 @@ from frappe.core.doctype.page.page import delete_custom_role from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles from frappe.desk.reportview import append_totals_row from six import iteritems +from frappe.utils.safe_exec import safe_exec class Report(Document): @@ -67,9 +68,7 @@ class Report(Document): if not allowed: return True - roles = frappe.get_roles() - - if has_common(roles, allowed): + if has_common(frappe.get_roles(), allowed): return True def update_report_json(self): @@ -99,13 +98,13 @@ class Report(Document): # The JOB if self.is_standard == 'Yes': module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module") - method_name = get_report_module_dotted_path(module, report.name) + ".execute" + method_name = get_report_module_dotted_path(module, self.name) + ".execute" res = frappe.get_attr(method_name)(frappe._dict(filters)) else: if not frappe.conf.server_script_enabled: raise ServerScriptNotEnabled loc = {"filters": frappe._dict(filters), 'data':[]} - exec(self.report_script, globals(), loc) + safe_exec(self.report_script, None, loc) res = loc['data'] end_time = datetime.datetime.now() @@ -229,3 +228,7 @@ class Report(Document): def is_prepared_report_disabled(report): return frappe.db.get_value('Report', report, 'disable_prepared_report') or 0 + +def get_report_module_dotted_path(module, report_name): + return frappe.local.module_app[scrub(module)] + "." + scrub(module) \ + + ".report." + scrub(report_name) + "." + scrub(report_name) diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index ad21675ee5..cea3fc0096 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -6,6 +6,7 @@ import frappe, json, os import unittest test_records = frappe.get_test_records('Report') +test_dependencies = ['User'] class TestReport(unittest.TestCase): def test_report_builder(self): @@ -28,7 +29,8 @@ class TestReport(unittest.TestCase): self.assertEqual(columns[1].get('label'), 'Module') self.assertTrue('User' in [d[0] for d in data]) - def test_report_permisisons(self): + def test_report_permissions(self): + frappe.set_user('test@example.com') frappe.db.sql("""delete from `tabHas Role` where parent = %s and role = 'Test Has Role'""", frappe.session.user, auto_commit=1) @@ -53,6 +55,7 @@ class TestReport(unittest.TestCase): report = frappe.get_doc('Report', 'Test Report') self.assertNotEquals(report.is_permitted(), True) + frappe.set_user('Administrator') # test for the `_format` method if report data doesn't have sort_by parameter def test_format_method(self): @@ -70,4 +73,30 @@ class TestReport(unittest.TestCase): frappe.delete_doc('Report', 'User Activity Report Without Sort') def test_non_standard_script_report(self): - pass + report_name = 'Test Non Standard Script Report' + if not frappe.db.exists("Report", report_name): + report = frappe.get_doc({ + 'doctype': 'Report', + 'ref_doctype': 'User', + 'report_name': report_name, + 'report_type': 'Script Report', + 'is_standard': 'No', + }).insert(ignore_permissions=True) + else: + report = frappe.get_doc('Report', report_name) + + report.report_script = ''' +data = [ + [{'fieldname': 'name', 'label': 'ID'}], + [frappe.db.get_all('User', dict(user_type="System User"))] +] +''' + report.save() + data = report.get_data() + + # check columns + self.assertEqual(data[0][0]['label'], 'ID') + + # check values + self.assertTrue('Administrator' in [d.get('name') for d in data[1][0]]) + diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 192290c127..21a69f5111 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -338,11 +338,6 @@ def build_xlsx_data(columns, data, visible_idx,include_indentation): return result - -def get_report_module_dotted_path(module, report_name): - return frappe.local.module_app[scrub(module)] + "." + scrub(module) \ - + ".report." + scrub(report_name) + "." + scrub(report_name) - def add_total_row(result, columns, meta = None): total_row = [""]*len(columns) has_percent = [] diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 89acd14d2f..a48644d1dc 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -33,35 +33,38 @@ def get_safe_globals(): out = frappe._dict( # make available limited methods of frappe - frappe = frappe._dict({ - "_": frappe._, - 'flags': frappe.flags, - "get_url": frappe.utils.get_url, - 'format': frappe.format_value, - "format_value": frappe.format_value, - 'date_format': date_format, - "format_date": frappe.utils.data.global_date_format, - "form_dict": getattr(frappe.local, 'form_dict', {}), - "get_hooks": frappe.get_hooks, - "get_meta": frappe.get_meta, - "get_doc": frappe.get_doc, - "get_cached_doc": frappe.get_cached_doc, - "get_list": frappe.get_list, - "get_all": frappe.get_all, - 'get_system_settings': frappe.get_system_settings, - "utils": datautils, - "user": user, - "get_fullname": frappe.utils.get_fullname, - "get_gravatar": frappe.utils.get_gravatar_url, - "full_name": frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest", - "render_template": frappe.render_template, - "request": getattr(frappe.local, 'request', {}), - 'session': frappe._dict( - user = user, + json = json, + dict = dict, + frappe = frappe._dict( + _ = frappe._, + _dict = frappe._dict, + flags = frappe.flags, + get_url = frappe.utils.get_url, + format = frappe.format_value, + format_value = frappe.format_value, + date_format = date_format, + format_date = frappe.utils.data.global_date_format, + form_dict = getattr(frappe.local, 'form_dict', {}), + get_hooks = frappe.get_hooks, + get_meta = frappe.get_meta, + get_doc = frappe.get_doc, + get_cached_doc = frappe.get_cached_doc, + get_list = frappe.get_list, + get_all = frappe.get_all, + get_system_settings = frappe.get_system_settings, + utils = datautils, + user = user, + get_fullname = frappe.utils.get_fullname, + get_gravatar = frappe.utils.get_gravatar_url, + full_name = frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest", + render_template = frappe.render_template, + request = getattr(frappe.local, 'request', {}), + session = frappe._dict( + user = user, csrf_token = frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' ), - "socketio_port": frappe.conf.socketio_port, - }), + socketio_port = frappe.conf.socketio_port, + ), style = frappe._dict( border_color = '#d1d8dd' ), @@ -72,7 +75,6 @@ def get_safe_globals(): scrub = scrub, guess_mimetype = mimetypes.guess_type, html2text = html2text, - json = json, dev_server = 1 if os.environ.get('DEV_SERVER', False) else 0 ) @@ -82,6 +84,8 @@ def get_safe_globals(): out.get_visible_columns = get_visible_columns out.frappe.date_format = date_format out.frappe.db = frappe._dict( + get_list = frappe.get_list, + get_all = frappe.get_all, get_value = frappe.db.get_value, get_single_value = frappe.db.get_single_value, get_default = frappe.db.get_default, @@ -94,10 +98,23 @@ def get_safe_globals(): out.update(safe_globals) # default writer allows write access - out._write_ = lambda obj: obj + out._write_ = _write + out._getitem_ = _getitem return out +def _getitem(obj, key): + # guard function for RestrictedPython + # allow any key to be accessed as long as it does not start with underscore + if isinstance(key, str) and key.startswith('_'): + raise SyntaxError('Key starts with _') + return obj[key] + +def _write(obj): + # guard function for RestrictedPython + # allow writing to any object + return obj + def add_module_properties(module, data, filter_method): for key, obj in module.__dict__.items(): if key.startswith("_"): From b84663621fa822a44ad584bba75098217719fa5d Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 14:16:35 +0530 Subject: [PATCH 16/29] fix(linting) --- frappe/core/doctype/server_script/server_script.py | 6 ++++-- frappe/core/doctype/server_script/test_server_script.py | 1 - frappe/tests/test_safe_exec.py | 1 - frappe/utils/safe_exec.py | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 50d64b2180..2330f1946a 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -11,10 +11,12 @@ from frappe.utils.safe_exec import safe_exec class ServerScriptNotEnabled(frappe.PermissionError): pass class ServerScript(Document): - def validate(self): + @staticmethod + def validate(): frappe.only_for('Script Manager') - def on_update(self): + @staticmethod + def on_update(): frappe.cache().delete_value('server_script_map') def execute_method(self): diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 287b18d7fa..3e6b7a3a98 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -7,7 +7,6 @@ import frappe import unittest import requests from frappe.utils import get_site_url -from frappe.core.doctype.server_script.server_script_utils import get_server_script_map scripts = [ dict( diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py index b868b4fcb8..65657b4739 100644 --- a/frappe/tests/test_safe_exec.py +++ b/frappe/tests/test_safe_exec.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals import unittest -import frappe from frappe.utils.safe_exec import safe_exec class TestSafeExec(unittest.TestCase): diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index a48644d1dc..a1cdbb2123 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -6,13 +6,12 @@ from RestrictedPython import compile_restricted, safe_globals def safe_exec(script, _globals=None, _locals=None): if not _globals: _globals = get_safe_globals() - exec(compile_restricted(script), _globals, _locals) + exec(compile_restricted(script), _globals, _locals) # pylint: disable=exec-used def get_safe_globals(): import frappe import frappe.utils import frappe.utils.data - from frappe.model.document import get_controller from frappe.website.utils import (get_shade, get_toc, get_next_link) from frappe.modules import scrub from frappe.www.printview import get_visible_columns From c57f12800090e2ab4b44dca839046fafa8088652 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 14:34:14 +0530 Subject: [PATCH 17/29] fix(minor): add comments --- frappe/__init__.py | 4 ++- frappe/core/doctype/report/report.py | 33 +++++++++++-------- .../doctype/server_script/server_script.py | 12 +++---- .../server_script/server_script_utils.py | 15 +++++++++ frappe/utils/safe_exec.py | 13 ++++++-- 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 1e801348b6..d3f40d8375 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -520,7 +520,7 @@ def read_only(): return wrapper_fn return innfn -def only_for(roles): +def only_for(roles, message=False): """Raise `frappe.PermissionError` if the user does not have any of the given **Roles**. :param roles: List of roles to check.""" @@ -532,6 +532,8 @@ def only_for(roles): roles = set(roles) myroles = set(get_roles()) if not roles.intersection(myroles): + if message: + msgprint(_('Only for {}'.format(', '.join(roles)))) raise PermissionError def get_domain_data(module): diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 830dfcd895..511ae5b814 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -29,7 +29,9 @@ class Report(Document): self.is_standard = "Yes" if self.is_standard == "No": - frappe.only_for('Script Manager') + # allow only script manager to edit scripts + if frappe.session.user!="Administrator": + frappe.only_for('Script Manager', True) if frappe.db.get_value("Report", self.name, "is_standard") == "Yes": frappe.throw(_("Cannot edit a standard report. Please duplicate and create a new report")) @@ -91,26 +93,20 @@ class Report(Document): make_boilerplate("controller.js", self, {"name": self.name}) def execute_script_report(self, filters): + # save the timestamp to automatically set to prepared threshold = 30 res = [] start_time = datetime.datetime.now() + # The JOB if self.is_standard == 'Yes': - module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module") - method_name = get_report_module_dotted_path(module, self.name) + ".execute" - res = frappe.get_attr(method_name)(frappe._dict(filters)) + self.execute_module(filters) else: - if not frappe.conf.server_script_enabled: - raise ServerScriptNotEnabled - loc = {"filters": frappe._dict(filters), 'data':[]} - safe_exec(self.report_script, None, loc) - res = loc['data'] - - end_time = datetime.datetime.now() - - execution_time = (end_time - start_time).seconds + self.execute_script(filters) + # automatically set as prepared + execution_time = (datetime.datetime.now() - start_time).total_seconds() if execution_time > threshold and not self.prepared_report: self.db_set('prepared_report', 1) @@ -118,6 +114,17 @@ class Report(Document): return res + def execute_module(self, filters): + # report in python module + module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module") + method_name = get_report_module_dotted_path(module, self.name) + ".execute" + res = frappe.get_attr(method_name)(frappe._dict(filters)) + + def execute_script(self, filters): + # server script + loc = {"filters": frappe._dict(filters), 'data':[]} + safe_exec(self.report_script, None, loc) + res = loc['data'] def get_data(self, filters=None, limit=None, user=None, as_dict=False): columns = [] diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 2330f1946a..add58f79b7 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -13,25 +13,23 @@ class ServerScriptNotEnabled(frappe.PermissionError): pass class ServerScript(Document): @staticmethod def validate(): - frappe.only_for('Script Manager') + frappe.only_for('Script Manager', True) @staticmethod def on_update(): frappe.cache().delete_value('server_script_map') def execute_method(self): - if not frappe.conf.server_script_enabled: - raise ServerScriptNotEnabled if self.script_type == 'API': + # validate if guest is allowed if frappe.session.user == 'Guest' and not self.allow_guest: raise frappe.PermissionError safe_exec(self.script) else: + # wrong report type! raise frappe.DoesNotExistError def execute_doc(self, doc): - if not frappe.conf.server_script_enabled: - raise ServerScriptNotEnabled - context = dict(doc = doc) - safe_exec(self.script, None, context) + # execute event + safe_exec(self.script, None, dict(doc = doc)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 2bece465dd..cd02fae364 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -1,5 +1,8 @@ import frappe +# this is a separate file since it is imported in frappe.model.document +# to avoid circular imports + EVENT_MAP = { 'before_insert': 'Before Insert', 'after_insert': 'After Insert', @@ -14,12 +17,14 @@ EVENT_MAP = { } def run_server_script_api(method): + # called via handler, execute an API script script_name = get_server_script_map().get('_api', {}).get(method) if script_name: frappe.get_doc('Server Script', script_name).execute_method() return True def run_server_script_for_doc_event(doc, event): + # run document event method if not event in EVENT_MAP: return @@ -28,10 +33,20 @@ def run_server_script_for_doc_event(doc, event): scripts = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) if scripts: + # run all scripts for this doctype + event for script_name in scripts: frappe.get_doc('Server Script', script_name).execute_doc(doc) def get_server_script_map(): + # fetch cached server script methods + # { + # '[doctype]': { + # 'Before Insert': ['[server script 1]', '[server script 2]'] + # }, + # '_api': { + # '[path]': '[server script]' + # } + # } script_map = frappe.cache().get_value('server_script_map') if script_map is None: script_map = {} diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index a1cdbb2123..ce9ed694f0 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -5,8 +5,17 @@ from html2text import html2text from RestrictedPython import compile_restricted, safe_globals def safe_exec(script, _globals=None, _locals=None): - if not _globals: _globals = get_safe_globals() - exec(compile_restricted(script), _globals, _locals) # pylint: disable=exec-used + # script reports must be enabled via site_config.json + if not frappe.conf.server_script_enabled: + frappe.msgprint('Please Enable Server Scripts') + raise ServerScriptNotEnabled + + # build globals + exec_globals = get_safe_globals() + exec_globals.update(_globals) + + # execute script compiled by RestrictedPython + exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used def get_safe_globals(): import frappe From a1a1200adb61c170c15c05cc3f6ef59f2cbb6b2f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 14:47:25 +0530 Subject: [PATCH 18/29] fix(imports) --- frappe/utils/safe_exec.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index ce9ed694f0..8c4676554a 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -3,6 +3,13 @@ import os, json, inspect import mimetypes from html2text import html2text from RestrictedPython import compile_restricted, safe_globals +import frappe +import frappe.utils +import frappe.utils.data +from frappe.website.utils import (get_shade, get_toc, get_next_link) +from frappe.modules import scrub +from frappe.www.printview import get_visible_columns +import frappe.exceptions def safe_exec(script, _globals=None, _locals=None): # script reports must be enabled via site_config.json @@ -18,14 +25,6 @@ def safe_exec(script, _globals=None, _locals=None): exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used def get_safe_globals(): - import frappe - import frappe.utils - import frappe.utils.data - from frappe.website.utils import (get_shade, get_toc, get_next_link) - from frappe.modules import scrub - from frappe.www.printview import get_visible_columns - import frappe.exceptions - datautils = {} if frappe.db: date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd" From 3d22ecc5f37396cecdf79072a4279c2f734aa388 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 15:05:39 +0530 Subject: [PATCH 19/29] fix(minor) --- frappe/core/doctype/report/report.py | 8 ++++---- frappe/utils/safe_exec.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 511ae5b814..1c62ff131b 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -101,9 +101,9 @@ class Report(Document): # The JOB if self.is_standard == 'Yes': - self.execute_module(filters) + res = self.execute_module(filters) else: - self.execute_script(filters) + res = self.execute_script(filters) # automatically set as prepared execution_time = (datetime.datetime.now() - start_time).total_seconds() @@ -118,13 +118,13 @@ class Report(Document): # report in python module module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module") method_name = get_report_module_dotted_path(module, self.name) + ".execute" - res = frappe.get_attr(method_name)(frappe._dict(filters)) + return frappe.get_attr(method_name)(frappe._dict(filters)) def execute_script(self, filters): # server script loc = {"filters": frappe._dict(filters), 'data':[]} safe_exec(self.report_script, None, loc) - res = loc['data'] + return loc['data'] def get_data(self, filters=None, limit=None, user=None, as_dict=False): columns = [] diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 8c4676554a..a2d987e61d 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -19,7 +19,8 @@ def safe_exec(script, _globals=None, _locals=None): # build globals exec_globals = get_safe_globals() - exec_globals.update(_globals) + if _globals: + exec_globals.update(_globals) # execute script compiled by RestrictedPython exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used @@ -46,31 +47,36 @@ def get_safe_globals(): _ = frappe._, _dict = frappe._dict, flags = frappe.flags, - get_url = frappe.utils.get_url, + format = frappe.format_value, format_value = frappe.format_value, date_format = date_format, format_date = frappe.utils.data.global_date_format, form_dict = getattr(frappe.local, 'form_dict', {}), - get_hooks = frappe.get_hooks, + get_meta = frappe.get_meta, get_doc = frappe.get_doc, get_cached_doc = frappe.get_cached_doc, get_list = frappe.get_list, get_all = frappe.get_all, get_system_settings = frappe.get_system_settings, + utils = datautils, + get_url = frappe.utils.get_url, + render_template = frappe.render_template, + msgprint = frappe.msgprint, + user = user, get_fullname = frappe.utils.get_fullname, get_gravatar = frappe.utils.get_gravatar_url, full_name = frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest", - render_template = frappe.render_template, request = getattr(frappe.local, 'request', {}), session = frappe._dict( user = user, csrf_token = frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' ), socketio_port = frappe.conf.socketio_port, + get_hooks = frappe.get_hooks, ), style = frappe._dict( border_color = '#d1d8dd' From 4a3965285021cc0453389a4fa009debe456123e7 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 15:31:45 +0530 Subject: [PATCH 20/29] fix(minor) --- frappe/core/doctype/server_script/server_script.py | 2 -- frappe/utils/safe_exec.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index add58f79b7..e2c6d3b7b0 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -8,8 +8,6 @@ import frappe from frappe.model.document import Document from frappe.utils.safe_exec import safe_exec -class ServerScriptNotEnabled(frappe.PermissionError): pass - class ServerScript(Document): @staticmethod def validate(): diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index a2d987e61d..f1f37dea5c 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -11,6 +11,8 @@ from frappe.modules import scrub from frappe.www.printview import get_visible_columns import frappe.exceptions +class ServerScriptNotEnabled(frappe.PermissionError): pass + def safe_exec(script, _globals=None, _locals=None): # script reports must be enabled via site_config.json if not frappe.conf.server_script_enabled: From 37a38c0a5650e03c7e5790fcadc2f342fd2c982d Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sat, 12 Oct 2019 11:07:27 +0530 Subject: [PATCH 21/29] fix: code break on undefined/null/'' values --- frappe/public/js/frappe/form/toolbar.js | 59 ++++++++++++++----------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index b3780cb177..ac593994d7 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -58,7 +58,7 @@ frappe.ui.form.Toolbar = Class.extend({ if (title_field && this.frm.perm[0].write && !this.frm.doc.__islocal - && !doc_field.options + && doc_field.fieldtype === "Data" && !doc_field.read_only) { return true; } else { @@ -73,11 +73,12 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.$title_area.find(".title-text").on("click", () => { let fields = []; - let title_field = me.frm.meta.title_field; - let title_field_label = me.frm.get_docfield(title_field).label + let title_field = me.frm.meta.title_field || ''; // check if title is updateable if (me.is_title_editable()) { + let title_field_label = me.frm.get_docfield(title_field).label + fields.push({ label: __("New {0}", [__(title_field_label)]), fieldname: "title", @@ -113,36 +114,42 @@ frappe.ui.form.Toolbar = Class.extend({ d.set_primary_action(__("Rename"), function () { let args = d.get_values(); - if (args.title && me.frm.doc[title_field] != args.title) { - me.frm.set_value(title_field, args.title); - me.frm.save_or_update(); - } - if (args.name && me.frm.doc.name != args.name) { - rename_doc(d, me.frm.doctype, me.frm.doc.name, args); - } + frappe.run_serially([ + rename_doc(d, me.frm.doctype, me.frm.doc.name, args), + me.frm.reload_doc(), + update_title(me, title_field, args) + ]) d.hide(); }); } }); - + function update_title(me, title_field, args) { + if (args.title && me.frm.doc[title_field] != args.title) { + me.frm.set_value(title_field, args.title); + me.frm.save_or_update(); + } + } function rename_doc(d, doctype, docname, args) { - frappe.call({ - method: "frappe.model.rename_doc.rename_doc", - args: { - doctype: doctype, - old: docname, - new: args.name, - merge: args.merge - }, - btn: d.get_primary_btn(), - callback: function (res) { - if (!res.exc) { - $(document).trigger('rename', [doctype, docname, res.message || args.name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + if (args.name && docname != args.name) { + frappe.call({ + method: "frappe.model.rename_doc.rename_doc", + args: { + doctype: doctype, + old: docname, + new: args.name, + merge: args.merge + }, + btn: d.get_primary_btn(), + callback: function (res) { + if (!res.exc) { + $(document).trigger('rename', [doctype, docname, res.message || args.name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + } + return res } - } - }); + }); + } } }, get_dropdown_menu: function(label) { From 12d1d5a649f8696ab22fd6ebd033d030abec5606 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 15 Oct 2019 00:16:18 +0530 Subject: [PATCH 22/29] fix: shifted tasks to server-side via frappe.call --- frappe/model/rename_doc.py | 16 ++++++++ frappe/public/js/frappe/form/toolbar.js | 53 ++++++++++--------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index f13290d856..8c5a847ad5 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -10,6 +10,22 @@ from frappe.model.dynamic_links import get_dynamic_link_map from frappe.utils.password import rename_password from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data + +@frappe.whitelist() +def update_document_title(doctype, document, title_field, old_title, new_title, old_name, new_name): + """ + Update title from header in form view + """ + if new_title and old_title != new_title: + frappe.db.set_value(doctype, document, title_field, new_title) + frappe.msgprint(_('Saved'), alert=True, indicator='green') + + if new_name and old_name != new_name: + return rename_doc(doctype, old_name, new_name) + + return old_name + + @frappe.whitelist() def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False): """ diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index ac593994d7..a3d28c0644 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -73,6 +73,8 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.$title_area.find(".title-text").on("click", () => { let fields = []; + let doctype = me.frm.doctype; + let docname = me.frm.doc.name; let title_field = me.frm.meta.title_field || ''; // check if title is updateable @@ -95,7 +97,7 @@ frappe.ui.form.Toolbar = Class.extend({ fieldname: "name", fieldtype: "Data", reqd: 1, - default: me.frm.doc.name + default: docname }, { label: __("Merge with existing"), fieldname: "merge", @@ -103,6 +105,7 @@ frappe.ui.form.Toolbar = Class.extend({ default: 0 }]); } + // create dialog if (fields.length > 0) { let d = new frappe.ui.Dialog({ @@ -113,41 +116,25 @@ frappe.ui.form.Toolbar = Class.extend({ d.set_primary_action(__("Rename"), function () { let args = d.get_values(); - - frappe.run_serially([ - rename_doc(d, me.frm.doctype, me.frm.doc.name, args), - me.frm.reload_doc(), - update_title(me, title_field, args) - ]) - - d.hide(); - }); - } - }); - function update_title(me, title_field, args) { - if (args.title && me.frm.doc[title_field] != args.title) { - me.frm.set_value(title_field, args.title); - me.frm.save_or_update(); - } - } - function rename_doc(d, doctype, docname, args) { - if (args.name && docname != args.name) { - frappe.call({ - method: "frappe.model.rename_doc.rename_doc", - args: { - doctype: doctype, - old: docname, - new: args.name, - merge: args.merge - }, - btn: d.get_primary_btn(), - callback: function (res) { - if (!res.exc) { + frappe.call({ + method: "frappe.model.rename_doc.update_document_title", + args: { + doctype: doctype, + document: docname, + title_field: title_field || null, + old_title: me.frm.doc[title_field] || null, + new_title: args.title || null, + old_name: docname || null, + new_name: args.name || null + }, + btn: d.get_primary_btn() + }).then((res) => { + if (!res.exc && (args.name != docname)) { $(document).trigger('rename', [doctype, docname, res.message || args.name]); if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; } - return res - } + }); + d.hide(); }); } } From 6966176e4100ec0f59819746268df0eddd879904 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Oct 2019 09:40:09 +0530 Subject: [PATCH 23/29] fix: broken js syntax in toolbar.js --- frappe/public/js/frappe/form/toolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index a3d28c0644..b3a961c679 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -137,7 +137,7 @@ frappe.ui.form.Toolbar = Class.extend({ d.hide(); }); } - } + }); }, get_dropdown_menu: function(label) { return this.page.add_dropdown(label); From 14e9ed6f9941a2c4f494f85eb5f09601f0433b99 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 16 Oct 2019 12:13:23 +0530 Subject: [PATCH 24/29] fix(patch) --- frappe/core/doctype/server_script/server_script_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index cd02fae364..e327401331 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -47,6 +47,9 @@ def get_server_script_map(): # '[path]': '[server script]' # } # } + if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'): + return {} + script_map = frappe.cache().get_value('server_script_map') if script_map is None: script_map = {} From c4e4247ae121a92ad8c8eae9d77fb0a2131b55a6 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Oct 2019 23:57:05 +0530 Subject: [PATCH 25/29] fix: added alert to show unchanged status of document --- frappe/public/js/frappe/form/toolbar.js | 44 +++++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index b3a961c679..ddd9f0e22a 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -116,24 +116,32 @@ frappe.ui.form.Toolbar = Class.extend({ d.set_primary_action(__("Rename"), function () { let args = d.get_values(); - frappe.call({ - method: "frappe.model.rename_doc.update_document_title", - args: { - doctype: doctype, - document: docname, - title_field: title_field || null, - old_title: me.frm.doc[title_field] || null, - new_title: args.title || null, - old_name: docname || null, - new_name: args.name || null - }, - btn: d.get_primary_btn() - }).then((res) => { - if (!res.exc && (args.name != docname)) { - $(document).trigger('rename', [doctype, docname, res.message || args.name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; - } - }); + if (args.title != me.frm.doc[title_field] || args.name != docname) { + frappe.call({ + method: "frappe.model.rename_doc.update_document_title", + args: { + doctype: doctype, + document: docname, + title_field: title_field || null, + old_title: me.frm.doc[title_field] || null, + new_title: args.title || null, + old_name: docname || null, + new_name: args.name || null + }, + btn: d.get_primary_btn() + }).then((res) => { + me.frm.reload_doc(); + if (!res.exc && (args.name != docname)) { + $(document).trigger("rename", [doctype, docname, res.message || args.name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + } + }); + } else { + frappe.show_alert({ + indicator: "yellow", + message: __("Unchanged") + }); + } d.hide(); }); } From eab77f857dea961dc3b8fe445baebf391e42328c Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 21 Oct 2019 10:46:24 +0530 Subject: [PATCH 26/29] fix: Throttle clear cache --- frappe/public/js/frappe/ui/toolbar/toolbar.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js index 9bca9275b6..b98ef94887 100644 --- a/frappe/public/js/frappe/ui/toolbar/toolbar.js +++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js @@ -225,19 +225,16 @@ $.extend(frappe.ui.toolbar, { }, }); -frappe.ui.toolbar.clear_cache = function() { +frappe.ui.toolbar.clear_cache = frappe.utils.throttle(function() { frappe.assets.clear_local_storage(); - frappe.call({ - method: 'frappe.sessions.clear', - callback: function(r) { - if(!r.exc) { - frappe.show_alert({message:r.message, indicator:'green'}); - location.reload(true); - } - } + frappe.xcall('frappe.sessions.clear').then(message => { + frappe.show_alert({ + message: message, + indicator: 'green' + }); + location.reload(true); }); - return false; -}; +}, 10000); frappe.ui.toolbar.show_about = function() { try { From f1ba214b0b151b3f9b87c6ca6667334e75861d40 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 21 Oct 2019 15:24:26 +0530 Subject: [PATCH 27/29] feat: merge gzip and mysqldump to speedup backup (#8524) --- frappe/utils/backups.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 4ec68e536d..984e28db5e 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -63,7 +63,7 @@ class BackupGenerator: site = site.replace('.', '_') #Generate a random name using today's date and a 8 digit random number - for_db = todays_date + "-" + site + "-database.sql" + for_db = todays_date + "-" + site + "-database.sql.gz" for_public_files = todays_date + "-" + site + "-files.tar" for_private_files = todays_date + "-" + site + "-private-files.tar" backup_path = get_backup_path() @@ -111,14 +111,9 @@ class BackupGenerator: args = dict([item[0], frappe.utils.esc(item[1], '$ ')] for item in self.__dict__.copy().items()) - cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s > %(backup_path_db)s """ % args + cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s | gzip > %(backup_path_db)s """ % args err, out = frappe.utils.execute_in_shell(cmd_string) - cmd_string = 'gzip %(backup_path_db)s '% args - err, out = frappe.utils.execute_in_shell(cmd_string) - - self.backup_path_db = "{0}.gz".format(self.backup_path_db) - def send_email(self): """ Sends the link to backup file located at erpnext/backups From 83225791d887a4919ffb43a6dee5a8ae295d4190 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 21 Oct 2019 16:15:39 +0530 Subject: [PATCH 28/29] fix: cleanup --- frappe/model/rename_doc.py | 4 ++-- frappe/public/js/frappe/form/toolbar.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 8c5a847ad5..9b1bf09c4c 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -12,12 +12,12 @@ from frappe.model.utils.user_settings import sync_user_settings, update_user_set @frappe.whitelist() -def update_document_title(doctype, document, title_field, old_title, new_title, old_name, new_name): +def update_document_title(doctype, docname, title_field, old_title, new_title, old_name, new_name): """ Update title from header in form view """ if new_title and old_title != new_title: - frappe.db.set_value(doctype, document, title_field, new_title) + frappe.db.set_value(doctype, docname, title_field, new_title) frappe.msgprint(_('Saved'), alert=True, indicator='green') if new_name and old_name != new_name: diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index ddd9f0e22a..fd4b40c3bc 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -120,13 +120,13 @@ frappe.ui.form.Toolbar = Class.extend({ frappe.call({ method: "frappe.model.rename_doc.update_document_title", args: { - doctype: doctype, - document: docname, - title_field: title_field || null, - old_title: me.frm.doc[title_field] || null, - new_title: args.title || null, - old_name: docname || null, - new_name: args.name || null + doctype, + docname, + title_field, + old_title: me.frm.doc[title_field], + new_title: args.title, + old_name: docname, + new_name: args.name }, btn: d.get_primary_btn() }).then((res) => { From 227e6b21a4d69aafcb02cc6f68c3766166eb827f Mon Sep 17 00:00:00 2001 From: gavin Date: Mon, 21 Oct 2019 16:34:23 +0530 Subject: [PATCH 29/29] chore: syntax cleanup --- frappe/public/js/frappe/form/toolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index fd4b40c3bc..200640ee2b 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -79,7 +79,7 @@ frappe.ui.form.Toolbar = Class.extend({ // check if title is updateable if (me.is_title_editable()) { - let title_field_label = me.frm.get_docfield(title_field).label + let title_field_label = me.frm.get_docfield(title_field).label; fields.push({ label: __("New {0}", [__(title_field_label)]),