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/__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/cache_manager.py b/frappe/cache_manager.py index 4d5a7571ee..39ddc993ca 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", "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/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): 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..1c62ff131b 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals import frappe -import json -from frappe import _ +import json, datetime +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): @@ -27,16 +28,17 @@ 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": + # 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")) 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() @@ -68,9 +70,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): @@ -92,6 +92,40 @@ 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): + # save the timestamp to automatically set to prepared + threshold = 30 + res = [] + + start_time = datetime.datetime.now() + + # The JOB + if self.is_standard == 'Yes': + res = self.execute_module(filters) + else: + res = 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) + + frappe.cache().hset('report_execution_time', self.name, execution_time) + + 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" + 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) + return loc['data'] + def get_data(self, filters=None, limit=None, user=None, as_dict=False): columns = [] out = [] @@ -201,3 +235,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 258734743f..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): @@ -68,3 +71,32 @@ 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): + 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/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..82fff31394 --- /dev/null +++ b/frappe/core/doctype/server_script/server_script.json @@ -0,0 +1,98 @@ +{ + "autoname": "Prompt", + "creation": "2019-09-30 11:56:57.943241", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "script_type", + "disabled", + "column_break_3", + "reference_doctype", + "doctype_event", + "api_method", + "allow_guest", + "section_break_8", + "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 Document Type", + "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\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete" + }, + { + "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" + }, + { + "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-09 15:08:40.085059", + "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": "Script 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..e2c6d3b7b0 --- /dev/null +++ b/frappe/core/doctype/server_script/server_script.py @@ -0,0 +1,33 @@ +# -*- 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_exec import safe_exec + +class ServerScript(Document): + @staticmethod + def validate(): + frappe.only_for('Script Manager', True) + + @staticmethod + def on_update(): + frappe.cache().delete_value('server_script_map') + + def execute_method(self): + 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): + # 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 new file mode 100644 index 0000000000..e327401331 --- /dev/null +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -0,0 +1,64 @@ +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', + 'validate': 'Before Save', + 'on_update': 'After Save', + 'before_submit': 'Before Submit', + 'on_submit': 'After Submit', + 'before_cancel': 'Before Cancel', + 'on_cancel': 'After Cancel', + 'on_trash': 'Before Delete', + 'after_delete': 'After Delete', +} + +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 + + if frappe.flags.in_install: + return + + 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]' + # } + # } + 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 = {} + 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, {}).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) + + 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..3e6b7a3a98 --- /dev/null +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -0,0 +1,74 @@ +# -*- 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 + +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): + todo = frappe.get_doc(dict(doctype='ToDo', description='hello')).insert() + self.assertEqual(todo.status, 'Open') + + 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): + 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"]) 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/desk/query_report.py b/frappe/desk/query_report.py index 5d1829abb2..21a69f5111 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) @@ -355,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/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/model/rename_doc.py b/frappe/model/rename_doc.py index 12c57f2780..9b1bf09c4c 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -3,13 +3,29 @@ 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 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, 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, docname, 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): """ @@ -83,6 +99,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..200640ee2b 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 doc_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) { + && !this.frm.doc.__islocal + && doc_field.fieldtype === "Data" + && !doc_field.read_only) { return true; } else { return false; @@ -64,25 +68,82 @@ 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 = []; + let doctype = me.frm.doctype; + let docname = me.frm.doc.name; + 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", + fieldtype: "Data", + reqd: 1, + default: me.frm.doc[title_field] + }); } - if(me.can_rename()) { - me.frm.rename_doc(); + + // check if docname is updateable + if (me.can_rename()) { + fields.push(...[{ + label: __("New Name"), + fieldname: "name", + fieldtype: "Data", + reqd: 1, + default: docname + }, { + label: __("Merge with existing"), + fieldname: "merge", + fieldtype: "Check", + default: 0 + }]); + } + + // create dialog + if (fields.length > 0) { + let d = new frappe.ui.Dialog({ + title: __("Rename"), + fields: fields + }); + d.show(); + + d.set_primary_action(__("Rename"), function () { + let args = d.get_values(); + if (args.title != me.frm.doc[title_field] || args.name != docname) { + frappe.call({ + method: "frappe.model.rename_doc.update_document_title", + args: { + doctype, + docname, + title_field, + old_title: me.frm.doc[title_field], + new_title: args.title, + old_name: docname, + new_name: args.name + }, + 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(); + }); } }); }, diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js index 43ed31f96f..a7a14f5476 100644 --- a/frappe/public/js/frappe/ui/toolbar/toolbar.js +++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js @@ -215,19 +215,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 { diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py new file mode 100644 index 0000000000..65657b4739 --- /dev/null +++ b/frappe/tests/test_safe_exec.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import unittest +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/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 diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index 28e3b3d463..befb9336fa 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_exec 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_exec.py b/frappe/utils/safe_exec.py new file mode 100644 index 0000000000..f1f37dea5c --- /dev/null +++ b/frappe/utils/safe_exec.py @@ -0,0 +1,141 @@ + +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 + +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: + frappe.msgprint('Please Enable Server Scripts') + raise ServerScriptNotEnabled + + # build globals + exec_globals = get_safe_globals() + if _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(): + datautils = {} + if frappe.db: + date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd" + else: + date_format = 'yyyy-mm-dd' + + add_module_properties(frappe.utils.data, datautils, lambda obj: hasattr(obj, "__call__")) + + 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 + json = json, + dict = dict, + frappe = frappe._dict( + _ = frappe._, + _dict = frappe._dict, + flags = frappe.flags, + + 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_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", + 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' + ), + get_toc = get_toc, + get_next_link = get_next_link, + _ = frappe._, + get_shade = get_shade, + scrub = scrub, + guess_mimetype = mimetypes.guess_type, + html2text = html2text, + 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 + 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, + escape = frappe.db.escape, + ) + + if frappe.response: + out.frappe.response = frappe.response + + out.update(safe_globals) + + # default writer allows write access + 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("_"): + # ignore + continue + + if filter_method(obj): + # only allow functions + data[key] = obj 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, 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