diff --git a/frappe/__init__.py b/frappe/__init__.py
index 36a8b48ecd..46792e82a8 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -182,6 +182,7 @@ def init(site, sites_path=None, new_site=False):
local.meta_cache = {}
local.form_dict = _dict()
local.session = _dict()
+ local.dev_server = os.environ.get('DEV_SERVER', False)
setup_module_map()
diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json
index 7a1b845af3..0f9da802eb 100644
--- a/frappe/core/doctype/doctype_action/doctype_action.json
+++ b/frappe/core/doctype/doctype_action/doctype_action.json
@@ -8,7 +8,8 @@
"label",
"action_type",
"action",
- "group"
+ "group",
+ "hidden"
],
"fields": [
{
@@ -31,20 +32,28 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Action Type",
- "options": "Server Action",
+ "options": "Server Action\nRoute",
"reqd": 1
},
{
"columns": 4,
"fieldname": "action",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"in_list_view": 1,
- "label": "Action",
+ "label": "Action / Route",
"reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-09-24 09:11:39.860100",
+ "links": [],
+ "modified": "2020-08-21 14:44:03.845315",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Action",
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 3ed4076430..cc3995ad1d 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -7,12 +7,12 @@
"engine": "InnoDB",
"field_order": [
"script_type",
- "disabled",
- "column_break_3",
"reference_doctype",
"doctype_event",
"api_method",
"allow_guest",
+ "column_break_3",
+ "disabled",
"section_break_8",
"script",
"help_section",
@@ -85,8 +85,9 @@
"fieldtype": "HTML"
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-07 13:13:02.483963",
+ "modified": "2020-08-24 16:44:41.060350",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 539ae8eb01..839b784651 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -24,7 +24,8 @@ class ServerScript(Document):
# validate if guest is allowed
if frappe.session.user == 'Guest' and not self.allow_guest:
raise frappe.PermissionError
- safe_exec(self.script)
+ _globals, _locals = safe_exec(self.script)
+ return _globals.frappe.flags # output can be stored in flags
else:
# wrong report type!
raise frappe.DoesNotExistError
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 5c12858e8a..3356e584af 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -36,6 +36,15 @@ if "validate" in doc.description:
allow_guest = 1,
script = '''
frappe.response['message'] = 'hello'
+'''
+ ),
+ dict(
+ name='test_return_value',
+ script_type = 'API',
+ api_method = 'test_return_value',
+ allow_guest = 1,
+ script = '''
+frappe.flags = 'hello'
'''
)
]
@@ -73,3 +82,6 @@ class TestServerScript(unittest.TestCase):
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 test_api_return(self):
+ self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
diff --git a/frappe/custom/doctype/custom_script/custom_script.json b/frappe/custom/doctype/custom_script/custom_script.json
index fc086e4b0b..328b247c49 100644
--- a/frappe/custom/doctype/custom_script/custom_script.json
+++ b/frappe/custom/doctype/custom_script/custom_script.json
@@ -1,187 +1,91 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2013-01-10 16:34:01",
- "custom": 0,
- "description": "Adds a client custom script to a DocType",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:01",
+ "description": "Adds a client custom script to a DocType",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "dt",
+ "enabled",
+ "script",
+ "sample"
+ ],
"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": "dt",
- "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": "DocType",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "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
- },
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "DocType",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 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": "script",
- "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": "Script",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "script",
- "oldfieldtype": "Code",
- "options": "JS",
- "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
- },
+ "fieldname": "script",
+ "fieldtype": "Code",
+ "label": "Script",
+ "oldfieldname": "script",
+ "oldfieldtype": "Code",
+ "options": "JS",
+ "show_days": 1,
+ "show_seconds": 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": "sample",
- "fieldtype": "HTML",
- "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": "Sample",
- "length": 0,
- "no_copy": 0,
- "options": "
Custom Script Help
\nCustom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started
\n\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n
",
- "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
+ "fieldname": "sample",
+ "fieldtype": "HTML",
+ "label": "Sample",
+ "options": "Custom Script Help
\nCustom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started
\n\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n
",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled",
+ "show_days": 1,
+ "show_seconds": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-glass",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2019-03-21 14:26:57.402994",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Custom Script",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-glass",
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-08-24 21:56:07.719579",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Script",
+ "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": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"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": "Administrator",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py b/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
index e89282885f..1cc54a0d1a 100644
--- a/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
@@ -5,12 +5,12 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
-from frappe.utils import get_source_value
+from frappe.utils.safe_exec import get_safe_globals
class DataMigrationMapping(Document):
def get_filters(self):
if self.condition:
- return frappe.safe_eval(self.condition, dict(frappe=frappe))
+ return frappe.safe_eval(self.condition, get_safe_globals())
def get_fields(self):
fields = []
@@ -64,9 +64,16 @@ def get_value_from_fieldname(field_map, fieldname_field, doc):
field_name = get_source_value(field_map, fieldname_field)
if field_name.startswith('eval:'):
- value = frappe.safe_eval(field_name[5:], dict(frappe=frappe))
+ value = frappe.safe_eval(field_name[5:], get_safe_globals())
elif field_name[0] in ('"', "'"):
value = field_name[1:-1]
else:
value = get_source_value(doc, field_name)
return value
+
+def get_source_value(source, key):
+ '''Get value from source (object or dict) based on key'''
+ if isinstance(source, dict):
+ return source.get(key)
+ else:
+ return getattr(source, key)
diff --git a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
index b2ce4606f8..473acfb3d0 100644
--- a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
+++ b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
@@ -6,7 +6,8 @@ from __future__ import unicode_literals
import frappe, json, math
from frappe.model.document import Document
from frappe import _
-from frappe.utils import get_source_value, cstr
+from frappe.utils import cstr
+from frappe.data_migration.doctype.data_migration_mapping.data_migration_mapping import get_source_value
class DataMigrationRun(Document):
def run(self):
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 1e3749e030..15b0bed699 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -128,7 +128,7 @@ CREATE TABLE `tabDocType Action` (
`label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `action` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index a946a7ee5c..eeb0eecd3f 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -128,7 +128,7 @@ CREATE TABLE "tabDocType Action" (
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"label" varchar(140) NOT NULL,
- "group" varchar(140) DEFAULT NULL,
+ "group" text DEFAULT NULL,
"action_type" varchar(140) NOT NULL,
"action" varchar(140) NOT NULL,
PRIMARY KEY ("name")
diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py
index b5129b60bb..58153ca6ce 100644
--- a/frappe/database/postgres/schema.py
+++ b/frappe/database/postgres/schema.py
@@ -49,7 +49,7 @@ class PostgresTable(DBTable):
elif col.fieldtype in ("Check"):
using_clause = "USING {}::smallint".format(col.fieldname)
- query.append("ALTER COLUMN {0} TYPE {1} {2}".format(
+ query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format(
col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length),
using_clause)
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 148ae87249..94a38a5304 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -203,7 +203,7 @@ class Workspace:
cards = cards + get_custom_reports_and_doctypes(self.doc.module)
if len(self.extended_cards):
- cards = cards + self.extended_cards
+ cards = merge_cards_based_on_label(cards + self.extended_cards)
default_country = frappe.db.get_default("country")
def _doctype_contains_a_record(name):
@@ -579,3 +579,16 @@ def update_onboarding_step(name, field, value):
"""
frappe.db.set_value("Onboarding Step", name, field, value)
+
+def merge_cards_based_on_label(cards):
+ """Merge cards with common label."""
+ cards_dict = {}
+ for card in cards:
+ if card.label in cards_dict:
+ links = loads(cards_dict[card.label].links) + loads(card.links)
+ cards_dict[card.label].update(dict(links=dumps(links)))
+ cards_dict[card.label] = cards_dict.pop(card.label)
+ else:
+ cards_dict[card.label] = card
+
+ return list(cards_dict.values())
\ No newline at end of file
diff --git a/frappe/desk/doctype/console_log/__init__.py b/frappe/desk/doctype/console_log/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/console_log/console_log.js b/frappe/desk/doctype/console_log/console_log.js
new file mode 100644
index 0000000000..1ef4fdce59
--- /dev/null
+++ b/frappe/desk/doctype/console_log/console_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Console Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/desk/doctype/console_log/console_log.json b/frappe/desk/doctype/console_log/console_log.json
new file mode 100644
index 0000000000..a9ae9717fd
--- /dev/null
+++ b/frappe/desk/doctype/console_log/console_log.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "autoname": "format:Log on {timestamp}",
+ "creation": "2020-08-18 19:56:12.336427",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "script",
+ "output"
+ ],
+ "fields": [
+ {
+ "fieldname": "script",
+ "fieldtype": "Code",
+ "in_list_view": 1,
+ "label": "Script",
+ "read_only": 1
+ },
+ {
+ "fieldname": "output",
+ "fieldtype": "Code",
+ "label": "Output",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-08-18 20:07:57.587344",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Console Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py
new file mode 100644
index 0000000000..635c4c1ba7
--- /dev/null
+++ b/frappe/desk/doctype/console_log/console_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ConsoleLog(Document):
+ pass
diff --git a/frappe/desk/doctype/console_log/test_console_log.py b/frappe/desk/doctype/console_log/test_console_log.py
new file mode 100644
index 0000000000..04dc4f241f
--- /dev/null
+++ b/frappe/desk/doctype/console_log/test_console_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestConsoleLog(unittest.TestCase):
+ pass
diff --git a/frappe/desk/doctype/system_console/__init__.py b/frappe/desk/doctype/system_console/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
new file mode 100644
index 0000000000..c7eac39490
--- /dev/null
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('System Console', {
+ onload: function(frm) {
+ frappe.ui.keys.add_shortcut({
+ shortcut: 'shift+enter',
+ action: () => frm.execute_action('Execute'),
+ page: frm.page,
+ description: __('Execute Console script'),
+ ignore_inputs: true,
+ });
+ },
+
+ refresh: function(frm) {
+ frm.disable_save();
+ frm.page.set_primary_action(__("Execute"), () => {
+ frm.execute_action('Execute');
+ });
+ }
+});
diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json
new file mode 100644
index 0000000000..14e36e6fd3
--- /dev/null
+++ b/frappe/desk/doctype/system_console/system_console.json
@@ -0,0 +1,68 @@
+{
+ "actions": [
+ {
+ "action": "#List/Console Log/List",
+ "action_type": "Route",
+ "label": "Logs"
+ },
+ {
+ "action": "frappe.desk.doctype.system_console.system_console.execute_code",
+ "action_type": "Server Action",
+ "hidden": 1,
+ "label": "Execute"
+ }
+ ],
+ "creation": "2020-08-18 17:44:35.647815",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "console",
+ "commit",
+ "output"
+ ],
+ "fields": [
+ {
+ "description": "To print output use log(text)",
+ "fieldname": "console",
+ "fieldtype": "Code",
+ "label": "Console",
+ "options": "Python"
+ },
+ {
+ "fieldname": "output",
+ "fieldtype": "Code",
+ "label": "Output",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "commit",
+ "fieldtype": "Check",
+ "label": "Commit"
+ }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-08-21 14:44:35.296877",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "System Console",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
new file mode 100644
index 0000000000..6c87ca8c36
--- /dev/null
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe.utils.safe_exec import safe_exec
+from frappe.model.document import Document
+
+class SystemConsole(Document):
+ def run(self):
+ frappe.only_for('System Manager')
+ try:
+ frappe.debug_log = []
+ safe_exec(self.console)
+ self.output = '\n'.join(frappe.debug_log)
+ except: # noqa: E722
+ self.output = frappe.get_traceback()
+
+ if self.commit:
+ frappe.db.commit()
+ else:
+ frappe.db.rollback()
+
+ frappe.get_doc(dict(
+ doctype='Console Log',
+ script=self.console,
+ output=self.output)).insert()
+ frappe.db.commit()
+
+@frappe.whitelist()
+def execute_code(doc):
+ console = frappe.get_doc(json.loads(doc))
+ console.run()
+ return console.as_dict()
\ No newline at end of file
diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py
new file mode 100644
index 0000000000..55ef199122
--- /dev/null
+++ b/frappe/desk/doctype/system_console/test_system_console.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestSystemConsole(unittest.TestCase):
+ def test_system_console(self):
+ system_console = frappe.get_doc('System Console')
+ system_console.console = 'log("hello")'
+ system_console.run()
+
+ self.assertEqual(system_console.output, 'hello')
+
+ system_console.console = 'log(frappe.db.get_value("DocType", "DocType", "module"))'
+ system_console.run()
+
+ self.assertEqual(system_console.output, 'Core')
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index ba0e5c2216..c28a40657f 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -130,7 +130,7 @@ class FormMeta(Meta):
def add_custom_script(self):
"""embed all require files"""
# custom script
- custom = frappe.db.get_value("Custom Script", {"dt": self.name}, "script") or ""
+ custom = frappe.db.get_value("Custom Script", {"dt": self.name, "enabled": 1}, "script") or ""
self.set("__custom_js", custom)
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index 0f1e8dc57c..3277d8e9ee 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -14,9 +14,6 @@ frappe.ui.form.on('Newsletter', {
});
}, "fa fa-play", "btn-success");
}
- if (!doc.__islocal && cint(doc.email_sent)) {
- frm.set_df_property('schedule_send', "read_only", 1);
- }
frm.events.setup_dashboard(frm);
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index 4804b3d6fa..1dd6115b43 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -15,7 +15,10 @@
"email_sent",
"newsletter_content",
"subject",
+ "content_type",
"message",
+ "message_md",
+ "message_html",
"send_unsubscribe_link",
"send_attachments",
"published",
@@ -37,8 +40,7 @@
"fieldname": "send_from",
"fieldtype": "Data",
"ignore_xss_filter": 1,
- "label": "Sender",
- "no_copy": 1
+ "label": "Sender"
},
{
"default": "0",
@@ -50,7 +52,8 @@
},
{
"fieldname": "newsletter_content",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Content"
},
{
"fieldname": "subject",
@@ -61,11 +64,12 @@
"reqd": 1
},
{
+ "depends_on": "eval: doc.content_type === 'Rich Text'",
"fieldname": "message",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Message",
- "reqd": 1
+ "mandatory_depends_on": "eval: doc.content_type === 'Rich Text'"
},
{
"default": "1",
@@ -87,16 +91,20 @@
"read_only": 1
},
{
+ "collapsible": 1,
"fieldname": "test_the_newsletter",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Testing"
},
{
"description": "A Lead with this Email Address should exist",
"fieldname": "test_email_id",
"fieldtype": "Data",
- "label": "Test Email Address"
+ "label": "Test Email Address",
+ "options": "Email"
},
{
+ "depends_on": "eval: doc.test_email_id",
"fieldname": "test_send",
"fieldtype": "Button",
"label": "Test",
@@ -117,7 +125,8 @@
"depends_on": "eval: doc.schedule_sending",
"fieldname": "schedule_send",
"fieldtype": "Datetime",
- "label": "Schedule Send"
+ "label": "Schedule Send",
+ "read_only_depends_on": "eval: doc.email_sent"
},
{
"default": "0",
@@ -125,11 +134,32 @@
"fieldtype": "Check",
"label": "Send Attachments"
},
+ {
+ "fieldname": "content_type",
+ "fieldtype": "Select",
+ "label": "Content Type",
+ "options": "Rich Text\nMarkdown\nHTML"
+ },
+ {
+ "depends_on": "eval:doc.content_type === 'Markdown'",
+ "fieldname": "message_md",
+ "fieldtype": "Markdown Editor",
+ "label": "Message (Markdown)",
+ "mandatory_depends_on": "eval:doc.content_type === 'Markdown'"
+ },
+ {
+ "depends_on": "eval:doc.content_type === 'HTML'",
+ "fieldname": "message_html",
+ "fieldtype": "HTML Editor",
+ "label": "Message (HTML)",
+ "mandatory_depends_on": "eval:doc.content_type === 'HTML'"
+ },
{
"default": "0",
"fieldname": "schedule_sending",
"fieldtype": "Check",
- "label": "Schedule Sending"
+ "label": "Schedule Sending",
+ "read_only_depends_on": "eval: doc.email_sent"
}
],
"has_web_view": 1,
@@ -139,7 +169,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2020-08-17 18:11:59.541686",
+ "modified": "2020-08-24 19:59:37.262500",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 849c21f768..0a0a13a6ce 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -8,12 +8,9 @@ import frappe.utils
from frappe import throw, _
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.verified_command import get_signed_params, verify_request
-from frappe.utils.background_jobs import enqueue
from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers
-from frappe.utils import parse_addr, now_datetime
-from frappe.utils import validate_email_address
-
+from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address
class Newsletter(WebsiteGenerator):
def onload(self):
@@ -29,8 +26,8 @@ class Newsletter(WebsiteGenerator):
def test_send(self, doctype="Lead"):
self.recipients = frappe.utils.split_emails(self.test_email_id)
- self.queue_all()
- frappe.msgprint(_("Scheduled to send to {0}").format(self.test_email_id))
+ self.queue_all(test_email=True)
+ frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
def send_emails(self):
"""send emails to leads and customers"""
@@ -40,21 +37,13 @@ class Newsletter(WebsiteGenerator):
self.recipients = self.get_recipients()
if self.recipients:
- if getattr(frappe.local, "is_ajax", False):
- self.validate_send()
- # using default queue with a longer timeout as this isn't a scheduled task
- enqueue(send_newsletter, queue='default', timeout=6000, event='send_newsletter',
- newsletter=self.name)
-
- else:
- self.queue_all()
-
- frappe.msgprint(_("Scheduled to send to {0} recipients").format(len(self.recipients)))
+ self.queue_all()
+ frappe.msgprint(_("Email queued to {0} recipients").format(len(self.recipients)))
else:
frappe.msgprint(_("Newsletter should have atleast one recipient"))
- def queue_all(self):
+ def queue_all(self, test_email=False):
if not self.get("recipients"):
# in case it is called via worker
self.recipients = self.get_recipients()
@@ -80,7 +69,7 @@ class Newsletter(WebsiteGenerator):
frappe.throw(_("Unable to find attachment {0}").format(file.name))
send(recipients=self.recipients, sender=sender,
- subject=self.subject, message=self.message,
+ subject=self.subject, message=self.get_message(),
reference_doctype=self.doctype, reference_name=self.name,
add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments,
unsubscribe_method="/unsubscribe",
@@ -90,9 +79,18 @@ class Newsletter(WebsiteGenerator):
if not frappe.flags.in_test:
frappe.db.auto_commit_on_many_writes = False
- self.db_set("email_sent", 1)
- self.db_set("schedule_send", now_datetime())
- self.db_set("scheduled_to_send", len(self.recipients))
+ if not test_email:
+ self.db_set("email_sent", 1)
+ self.db_set("schedule_send", now_datetime())
+ self.db_set("scheduled_to_send", len(self.recipients))
+
+ def get_message(self):
+
+ return {
+ 'Rich Text': self.message,
+ 'Markdown': markdown(self.message_md),
+ 'HTML': self.message_html
+ }[self.content_type]
def get_recipients(self):
"""Get recipients from Email Group"""
diff --git a/frappe/email/doctype/newsletter/newsletter_list.js b/frappe/email/doctype/newsletter/newsletter_list.js
index e95d29545d..9ded6148e0 100644
--- a/frappe/email/doctype/newsletter/newsletter_list.js
+++ b/frappe/email/doctype/newsletter/newsletter_list.js
@@ -1,8 +1,10 @@
frappe.listview_settings['Newsletter'] = {
- add_fields: ["subject", "email_sent"],
+ add_fields: ["subject", "email_sent", "schedule_sending"],
get_indicator: function(doc) {
- if(doc.email_sent) {
+ if (doc.email_sent) {
return [__("Sent"), "green", "email_sent,=,Yes"];
+ } else if (doc.schedule_sending) {
+ return [__("Scheduled"), "orange", "email_sent,=,No|schedule_sending,=,Yes"];
} else {
return [__("Not Sent"), "orange", "email_sent,=,No"];
}
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index bb339165d3..ee7f123b7e 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -67,6 +67,7 @@ class TestNewsletter(unittest.TestCase):
"doctype": "Newsletter",
"subject": "_Test Newsletter",
"send_from": "Test Sender ",
+ "content_type": "Rich Text",
"message": "Testing my news.",
"published": published,
"schedule_sending": bool(schedule_send),
diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js
index 454514f922..059f5518fc 100644
--- a/frappe/email/doctype/notification/notification.js
+++ b/frappe/email/doctype/notification/notification.js
@@ -19,9 +19,12 @@ frappe.notification = {
}
frappe.model.with_doctype(frm.doc.document_type, function() {
- let get_select_options = function(df) {
+ let get_select_options = function(df, parent_field) {
+ // Append parent_field name along with fieldname for child table fields
+ let select_value = parent_field ? df.fieldname + ',' + parent_field : df.fieldname;
+
return {
- value: df.fieldname,
+ value: select_value,
label: df.fieldname + ' (' + __(df.label) + ')'
};
};
@@ -59,9 +62,21 @@ frappe.notification = {
let receiver_fields = [];
if (frm.doc.channel === 'Email') {
receiver_fields = $.map(fields, function(d) {
- return d.options == 'Email' ||
- (d.options == 'User' && d.fieldtype == 'Link')
- ? get_select_options(d) : null;
+
+ // Add User and Email fields from child into select dropdown
+ if (d.fieldtype == 'Table') {
+ let child_fields = frappe.get_doc('DocType', d.options).fields;
+ return $.map(child_fields, function(df) {
+ return df.options == 'Email' ||
+ (df.options == 'User' && df.fieldtype == 'Link')
+ ? get_select_options(df, d.fieldname) : null;
+ });
+ // Add User and Email fields from parent into select dropdown
+ } else {
+ return d.options == 'Email' ||
+ (d.options == 'User' && d.fieldtype == 'Link')
+ ? get_select_options(d) : null;
+ }
});
} else if (in_list(['WhatsApp', 'SMS'], frm.doc.channel)) {
receiver_fields = $.map(fields, function(d) {
diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json
index 95f218ad73..8918709fa0 100644
--- a/frappe/email/doctype/notification/notification.json
+++ b/frappe/email/doctype/notification/notification.json
@@ -34,6 +34,7 @@
"set_property_after_alert",
"property_value",
"column_break_5",
+ "send_to_all_assignees",
"recipients",
"message_sb",
"message",
@@ -216,7 +217,7 @@
"fieldname": "recipients",
"fieldtype": "Table",
"label": "Recipients",
- "mandatory_depends_on": "eval:doc.channel!=='Slack'",
+ "mandatory_depends_on": "eval:doc.channel!=='Slack' && !doc.send_to_all_assignees",
"options": "Notification Recipient"
},
{
@@ -277,11 +278,19 @@
"fieldname": "send_system_notification",
"fieldtype": "Check",
"label": "Send System Notification"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.channel == 'Email'",
+ "fieldname": "send_to_all_assignees",
+ "fieldtype": "Check",
+ "label": "Send To All Assignees"
}
],
"icon": "fa fa-envelope",
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-11 19:24:35.479373",
+ "modified": "2020-09-01 18:36:22.550891",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 2ec208c89d..5355d56b35 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -10,6 +10,7 @@ from frappe.model.document import Document
from frappe.core.doctype.role.role import get_info_based_on_role, get_user_info
from frappe.utils import validate_email_address, nowdate, parse_val, is_html, add_to_date
from frappe.utils.jinja import validate_template
+from frappe.utils.safe_exec import get_safe_globals
from frappe.modules.utils import export_module_json, get_doc_module
from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
@@ -189,6 +190,7 @@ def get_context(context):
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
if not (recipients or cc or bcc):
return
+
sender = None
if self.sender and self.sender_email:
sender = formataddr((self.sender, self.sender_email))
@@ -234,13 +236,20 @@ def get_context(context):
if not frappe.safe_eval(recipient.condition, None, context):
continue
if recipient.receiver_by_document_field:
- email_ids_value = doc.get(recipient.receiver_by_document_field)
- if validate_email_address(email_ids_value):
- email_ids = email_ids_value.replace(",", "\n")
- recipients = recipients + email_ids.split("\n")
+ fields = recipient.receiver_by_document_field.split(',')
+ # fields from child table
+ if len(fields) > 1:
+ for d in doc.get(fields[1]):
+ email_id = d.get(fields[0])
+ if validate_email_address(email_id):
+ recipients.append(email_id)
+ # field from parent doc
+ else:
+ email_ids_value = doc.get(fields[0])
+ if validate_email_address(email_ids_value):
+ email_ids = email_ids_value.replace(",", "\n")
+ recipients = recipients + email_ids.split("\n")
- # else:
- # print "invalid email"
if recipient.cc and "{" in recipient.cc:
recipient.cc = frappe.render_template(recipient.cc, context)
@@ -262,6 +271,9 @@ def get_context(context):
for email in emails:
recipients = recipients + email.split("\n")
+ if self.send_to_all_assignees:
+ recipients = recipients + get_assignees(doc)
+
if not recipients and not cc and not bcc:
return None, None, None
return list(set(recipients)), list(set(cc)), list(set(bcc))
@@ -404,4 +416,13 @@ def evaluate_alert(doc, alert, event):
frappe.utils.get_link_to_form('Error Log', error_log.name)))
def get_context(doc):
- return {"doc": doc, "nowdate": nowdate, "frappe": frappe._dict(utils=frappe.utils)}
+ return {"doc": doc, "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
+
+def get_assignees(doc):
+ assignees = []
+ assignees = frappe.get_all('ToDo', filters={'status': 'Open', 'reference_name': doc.name,
+ 'reference_type': doc.doctype}, fields=['owner'])
+
+ recipients = [d.owner for d in assignees]
+
+ return recipients
\ No newline at end of file
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index 9bdf09375d..45a1587c1a 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe, frappe.utils, frappe.utils.scheduler
+from frappe.desk.form import assign_to
import unittest
test_records = frappe.get_test_records('Notification')
@@ -13,7 +14,31 @@ test_dependencies = ["User"]
class TestNotification(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabEmail Queue`""")
- frappe.set_user("test1@example.com")
+ frappe.set_user("test@example.com")
+
+ if not frappe.db.exists('Notification', {'name': 'ToDo Status Update'}, 'name'):
+ notification = frappe.new_doc('Notification')
+ notification.name = 'ToDo Status Update'
+ notification.subject = 'ToDo Status Update'
+ notification.document_type = 'ToDo'
+ notification.event = 'Value Change'
+ notification.value_changed = 'status'
+ notification.send_to_all_assignees = 1
+ notification.save()
+
+ if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
+ notification = frappe.new_doc('Notification')
+ notification.name = 'Contact Status Update'
+ notification.subject = 'Contact Status Update'
+ notification.document_type = 'Contact'
+ notification.event = 'Value Change'
+ notification.value_changed = 'status'
+ notification.message = 'Test Contact Update'
+ notification.append('recipients', {
+ 'receiver_by_document_field': 'email_id,email_ids'
+ })
+ notification.save()
+
def tearDown(self):
frappe.set_user("Administrator")
@@ -177,3 +202,65 @@ class TestNotification(unittest.TestCase):
frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""")
frappe.db.sql("""delete from `tabEmail Queue`""")
frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
+
+ def test_notification_to_assignee(self):
+ todo = frappe.new_doc('ToDo')
+ todo.description = 'Test Notification'
+ todo.save()
+
+ assign_to.add({
+ "assign_to": ["test2@example.com"],
+ "doctype": todo.doctype,
+ "name": todo.name,
+ "description": "Close this Todo"
+ })
+
+ assign_to.add({
+ "assign_to": ["test1@example.com"],
+ "doctype": todo.doctype,
+ "name": todo.name,
+ "description": "Close this Todo"
+ })
+
+ #change status of todo
+ todo.status = 'Closed'
+ todo.save()
+
+ email_queue = frappe.get_doc('Email Queue', {'reference_doctype': 'ToDo',
+ 'reference_name': todo.name})
+
+ self.assertTrue(email_queue)
+
+ recipients = [d.recipient for d in email_queue.recipients]
+ self.assertTrue('test2@example.com' in recipients)
+ self.assertTrue('test1@example.com' in recipients)
+
+ def test_notification_by_child_table_field(self):
+ contact = frappe.new_doc('Contact')
+ contact.first_name = 'John Doe'
+ contact.status = 'Open'
+ contact.append('email_ids', {
+ 'email_id': 'test2@example.com',
+ 'is_primary': 1
+ })
+
+ contact.append('email_ids', {
+ 'email_id': 'test1@example.com'
+ })
+
+ contact.save()
+
+ #change status of contact
+ contact.status = 'Replied'
+ contact.save()
+
+ email_queue = frappe.get_doc('Email Queue', {'reference_doctype': 'Contact',
+ 'reference_name': contact.name})
+
+ self.assertTrue(email_queue)
+
+ recipients = [d.recipient for d in email_queue.recipients]
+ self.assertTrue('test2@example.com' in recipients)
+ self.assertTrue('test1@example.com' in recipients)
+
+
diff --git a/frappe/email/doctype/notification_recipient/notification_recipient.json b/frappe/email/doctype/notification_recipient/notification_recipient.json
index 201899cd57..0670320a77 100644
--- a/frappe/email/doctype/notification_recipient/notification_recipient.json
+++ b/frappe/email/doctype/notification_recipient/notification_recipient.json
@@ -46,9 +46,10 @@
"options": "Role"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-02-21 11:18:40.125233",
+ "modified": "2020-09-01 17:40:27.289105",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification Recipient",
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index d545190c47..6d0c6f8f33 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -207,7 +207,11 @@ class EMail:
def set_in_reply_to(self, in_reply_to):
"""Used to send the Message-Id of a received email back as In-Reply-To"""
- self.msg_root["In-Reply-To"] = in_reply_to
+ try:
+ self.msg_root["In-Reply-To"] = in_reply_to
+ except ValueError:
+ # in_reply_to may contain line feed characters, so ignore in that case
+ pass
def make(self):
"""build into msg_root"""
diff --git a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
index 8ace4f57d3..bf96e4e27b 100644
--- a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
@@ -102,7 +102,7 @@ class DocumentTypeMapping(Document):
filters = json.loads(mapping.remote_value_filters)
for key, value in iteritems(filters):
if value.startswith('eval:'):
- val = frappe.safe_eval(value[5:], dict(frappe=frappe))
+ val = frappe.safe_eval(value[5:], None, dict(doc=doc))
filters[key] = val
if doc.get(value):
filters[key] = doc.get(value)
diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.json b/frappe/event_streaming/doctype/event_consumer/event_consumer.json
index d863677e03..85970dc277 100644
--- a/frappe/event_streaming/doctype/event_consumer/event_consumer.json
+++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.json
@@ -13,8 +13,7 @@
"api_secret",
"column_break_6",
"user",
- "incoming_change",
- "in_test"
+ "incoming_change"
],
"fields": [
{
@@ -60,14 +59,6 @@
"label": "Incoming Change",
"read_only": 1
},
- {
- "default": "0",
- "fieldname": "in_test",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "In Test",
- "read_only": 1
- },
{
"fieldname": "consumer_doctypes",
"fieldtype": "Table",
@@ -78,7 +69,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2019-12-30 11:52:16.276047",
+ "modified": "2020-09-06 15:42:00.746493",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Consumer",
diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
index a53d046be5..2e10c71d0d 100644
--- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py
+++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe
import json
import requests
+import os
from frappe.model.document import Document
from frappe.frappeclient import FrappeClient
from frappe.utils.data import get_url
@@ -14,10 +15,11 @@ from frappe.utils.background_jobs import get_jobs
class EventConsumer(Document):
def validate(self):
- if self.in_test:
+ # approve subscribed doctypes for tests
+ # frappe.flags.in_test won't work here as tests are running on the consumer site
+ if os.environ.get('CI'):
for entry in self.consumer_doctypes:
entry.status = 'Approved'
- self.in_test = False
def on_update(self):
if not self.incoming_change:
@@ -80,7 +82,6 @@ def register_consumer(data):
api_secret = frappe.generate_hash(length=10)
consumer.api_key = api_key
consumer.api_secret = api_secret
- consumer.in_test = data['in_test']
consumer.insert(ignore_permissions=True)
frappe.db.commit()
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index 73aea114ab..555b71f851 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -75,8 +75,7 @@ class EventProducer(Document):
return {
'event_consumer': get_url(),
'consumer_doctypes': json.dumps(consumer_doctypes),
- 'user': self.user,
- 'in_test': frappe.flags.in_test
+ 'user': self.user
}
def create_custom_fields(self):
@@ -110,8 +109,6 @@ class EventProducer(Document):
'status': get_approval_status(config, ref_doctype),
'unsubscribed': entry.unsubscribe
})
- if frappe.flags.in_test:
- event_consumer.in_test = True
event_consumer.user = self.user
event_consumer.incoming_change = True
producer_site.update(event_consumer)
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 8e6c8d58e4..f1556aa661 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -18,6 +18,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils.jinja import validate_template
+from frappe.utils.safe_exec import get_safe_globals
WEBHOOK_SECRET_HEADER = "X-Frappe-Webhook-Signature"
@@ -75,8 +76,7 @@ class Webhook(Document):
def get_context(doc):
- return {"doc": doc, "utils": frappe.utils}
-
+ return {'doc': doc, 'utils': get_safe_globals().get('frappe').get('utils')}
def enqueue_webhook(doc, webhook):
webhook = frappe.get_doc("Webhook", webhook.get("name"))
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index e59d325c9a..c39a73ccd7 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -134,7 +134,8 @@ log_types = (
'Notification Log',
'Email Queue',
'DocShare',
- 'Document Follow'
+ 'Document Follow',
+ 'Console Log'
)
def delete_fields(args_dict, delete=0):
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index ffaf84e2b3..f2c918113b 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -142,6 +142,8 @@ def parse_naming_series(parts, doctype='', doc=''):
part = today.strftime("%d")
elif e == 'YYYY':
part = today.strftime('%Y')
+ elif e == 'timestamp':
+ part = str(today)
elif e == 'FY':
part = frappe.defaults.get_user_default("fiscal_year")
elif e.startswith('{') and doc:
diff --git a/frappe/patches.txt b/frappe/patches.txt
index afe5a30abd..35389eee43 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -301,6 +301,9 @@ frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
frappe.patches.v13_0.add_standard_navbar_items
frappe.patches.v13_0.generate_theme_files_in_public_folder
frappe.patches.v13_0.increase_password_length
+frappe.patches.v12_0.fix_email_id_formatting
frappe.patches.v13_0.add_toggle_width_in_navbar_settings
frappe.patches.v13_0.rename_notification_fields
frappe.patches.v13_0.remove_duplicate_navbar_items
+frappe.patches.v13_0.enable_custom_script
+frappe.patches.v13_0.update_newsletter_content_type
diff --git a/frappe/patches/v12_0/fix_email_id_formatting.py b/frappe/patches/v12_0/fix_email_id_formatting.py
new file mode 100644
index 0000000000..03f606e0cc
--- /dev/null
+++ b/frappe/patches/v12_0/fix_email_id_formatting.py
@@ -0,0 +1,44 @@
+import frappe
+
+def execute():
+ fix_communications()
+ fix_show_as_cc_email_queue()
+ fix_email_queue_recipients()
+
+def fix_communications():
+ for communication in frappe.db.sql('''select name, recipients, cc, bcc from tabCommunication
+ where creation > '2020-06-01'
+ and communication_medium='Email'
+ and communication_type='Communication'
+ and (cc like '%<%' or bcc like '%<%' or recipients like '%<%')
+ ''', as_dict=1):
+
+ communication['recipients'] = format_email_id(communication.recipients)
+ communication['cc'] = format_email_id(communication.cc)
+ communication['bcc'] = format_email_id(communication.bcc)
+
+ frappe.db.sql('''update `tabCommunication` set recipients=%s,cc=%s,bcc=%s
+ where name =%s ''', (communication['recipients'], communication['cc'],
+ communication['bcc'], communication['name']))
+
+def fix_show_as_cc_email_queue():
+ for queue in frappe.get_all("Email Queue", {'creation': ['>', '2020-06-01'],
+ 'status': 'Not Sent', 'show_as_cc': ['like', '%<%']},
+ ['name', 'show_as_cc']):
+
+ frappe.db.set_value('Email Queue', queue['name'],
+ 'show_as_cc', format_email_id(queue['show_as_cc']))
+
+def fix_email_queue_recipients():
+ for recipient in frappe.db.sql('''select recipient, name from
+ `tabEmail Queue Recipient` where recipient like '%<%'
+ and status='Not Sent' and creation > '2020-06-01' ''', as_dict=1):
+
+ frappe.db.set_value('Email Queue Recipient', recipient['name'],
+ 'recipient', format_email_id(recipient['recipient']))
+
+def format_email_id(email):
+ if email and ('<' in email and '>' in email):
+ return email.replace('>', '>').replace('<', '<')
+
+ return email
diff --git a/frappe/patches/v13_0/enable_custom_script.py b/frappe/patches/v13_0/enable_custom_script.py
new file mode 100644
index 0000000000..92284e6dcc
--- /dev/null
+++ b/frappe/patches/v13_0/enable_custom_script.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ """Enable all the existing custom script"""
+ frappe.reload_doc("Custom", "doctype", "Custom Script")
+
+ frappe.db.sql("""
+ UPDATE `tabCustom Script` SET enabled=1
+ """)
\ No newline at end of file
diff --git a/frappe/patches/v13_0/update_newsletter_content_type.py b/frappe/patches/v13_0/update_newsletter_content_type.py
new file mode 100644
index 0000000000..6f8dcc1935
--- /dev/null
+++ b/frappe/patches/v13_0/update_newsletter_content_type.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('email', 'doctype', 'Newsletter')
+ frappe.db.sql("""
+ UPDATE tabNewsletter
+ SET content_type = 'Rich Text'
+ """)
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index 355f35891a..bbf9a89072 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -61,7 +61,7 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
// check if name exists
frappe.db.get_value(this.doctype, this.$input.val(),
'name', (val) => {
- if (val) {
+ if (val && val.name) {
this.set_description(__('{0} already exists. Select another name', [val.name]));
}
},
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index ff48ad2f60..f453b7dea3 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -321,22 +321,52 @@ frappe.ui.form.Form = class FrappeForm {
for (let action of this.meta.actions) {
frappe.ui.form.on(this.doctype, 'refresh', () => {
if (!this.is_new()) {
- this.add_custom_button(action.label, () => {
- if (action.action_type==='Server Action') {
- frappe.xcall(action.action, {doc: this.doc}).then(() => {
- frappe.msgprint({
- message: __('{} Complete', [action.label]),
- alert: true
- });
- });
- }
- }, action.group);
+ if (!action.hidden) {
+ this.add_custom_button(action.label, () => {
+ this.execute_action(action);
+ }, action.group);
+ }
}
});
}
}
}
+ execute_action(action) {
+ if (typeof action === 'string') {
+ // called by label - maybe via custom script
+ // frm.execute_action('Action')
+ for (let _action of this.meta.actions) {
+ if (_action.label === action) {
+ action = _action;
+ break;
+ }
+ }
+
+ if (typeof action === 'string') {
+ frappe.throw(`Action ${action} not found`);
+ }
+ }
+ if (action.action_type==='Server Action') {
+ frappe.xcall(action.action, {doc: this.doc}).then((doc) => {
+ if (doc.doctype) {
+ // document is returned by the method,
+ // apply the changes locally and refresh
+ frappe.model.sync(doc);
+ this.refresh();
+ }
+
+ // feedback
+ frappe.msgprint({
+ message: __('{} Complete', [action.label]),
+ alert: true
+ });
+ });
+ } else if (action.action_type==='Route') {
+ frappe.set_route(action.action);
+ }
+ }
+
switch_doc(docname) {
// record switch
if(this.docname != docname && (!this.meta.in_dialog || this.in_form) && !this.meta.istable) {
@@ -1405,19 +1435,16 @@ frappe.ui.form.Form = class FrappeForm {
}
set_read_only() {
- var perm = [];
- var docperms = frappe.perm.get_perm(this.doc.doctype);
- for (var i=0, l=docperms.length; i {
+ return {
read: p.read,
cancel: p.cancel,
share: p.share,
print: p.print,
email: p.email
};
- }
- this.perm = perm;
+ });
}
trigger(event, doctype, docname) {
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 366f746dc5..733c1bea5f 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -276,32 +276,32 @@ export default class GridRow {
make_column(df, colsize, txt, ci) {
let me = this;
- var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype) !== -1) ?
+ var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
" grid-overflow-no-ellipsis" : "");
- add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype) !== -1) ?
- " text-right" : "";
- add_class += (["Check"].indexOf(df.fieldtype) !== -1) ?
- " text-center" : "";
+ add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
+ " text-right": "";
+ add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
+ " text-center": "";
- var $col = $('')
+ var $col = $('')
.attr("data-fieldname", df.fieldname)
.attr("data-fieldtype", df.fieldtype)
.data("df", df)
.appendTo(this.row)
- .on('click', function () {
- if (frappe.ui.form.editable_row === me) {
+ .on('click', function() {
+ if(frappe.ui.form.editable_row===me) {
return;
}
var out = me.toggle_editable_row();
var col = this;
- setTimeout(function () {
+ setTimeout(function() {
$(col).find('input[type="Text"]:first').focus();
}, 500);
return out;
});
$col.field_area = $('').appendTo($col).toggle(false);
- $col.static_area = $('').appendTo($col).html(frappe.utils.escape_html(txt));
+ $col.static_area = $('').appendTo($col).html(txt);
$col.df = df;
$col.column_index = ci;
@@ -577,39 +577,39 @@ export default class GridRow {
var df = this.grid.get_docfield(fieldname) || undefined;
// format values if no frm
- if (!df) {
+ if(!df) {
df = this.grid.visible_columns.find((col) => {
return col[0].fieldname === fieldname;
});
- if (df && this.doc) {
+ if(df && this.doc) {
var txt = frappe.format(this.doc[fieldname], df[0],
null, this.doc);
}
}
- if (txt === undefined && this.frm) {
+ if(txt===undefined && this.frm) {
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}
// reset static value
var column = this.columns[fieldname];
- if (column) {
- column.static_area.html(frappe.utils.escape_html(txt) || "");
- if (df && df.reqd) {
- column.toggleClass('error', !!(txt === null || txt === ''));
+ if(column) {
+ column.static_area.html(txt || "");
+ if(df && df.reqd) {
+ column.toggleClass('error', !!(txt===null || txt===''));
}
}
// reset field value
var field = this.on_grid_fields_dict[fieldname];
- if (field) {
+ if(field) {
field.docname = this.doc.name;
field.refresh();
}
// in form
- if (this.grid_form) {
+ if(this.grid_form) {
this.grid_form.refresh_field(fieldname);
}
}
diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js
index f30368bbc3..2a1f52fbfb 100644
--- a/frappe/public/js/frappe/model/perm.js
+++ b/frappe/public/js/frappe/model/perm.js
@@ -42,7 +42,7 @@ $.extend(frappe.perm, {
},
get_perm: (doctype, doc) => {
- let perm = [{ read: 0 }];
+ let perm = [{ read: 0, permlevel: 0 }];
let meta = frappe.get_doc("DocType", doctype);
const user = frappe.session.user;
@@ -53,7 +53,7 @@ $.extend(frappe.perm, {
if (!meta) return perm;
- frappe.perm.build_role_permissions(perm, meta);
+ perm = frappe.perm.get_role_permissions(meta);
if (doc) {
// apply user permissions via docinfo (which is processed server-side)
@@ -107,35 +107,30 @@ $.extend(frappe.perm, {
return perm;
},
- build_role_permissions: (perm, meta) => {
+ get_role_permissions: (meta) => {
+ let perm = [{ read: 0, permlevel: 0 }];
// Returns a `dict` of evaluated Role Permissions
- $.each(meta.permissions || [], (i, p) => {
+ (meta.permissions || []).forEach(p => {
// if user has this role
- if (frappe.user_roles.includes(p.role)) {
- let permlevel = cint(p.permlevel);
- if (!perm[permlevel]) {
- perm[permlevel] = {};
- perm[permlevel]["permlevel"] = permlevel
- }
+ let permlevel = cint(p.permlevel);
+ if (!perm[permlevel]) {
+ perm[permlevel] = {};
+ perm[permlevel]["permlevel"] = permlevel;
+ }
- $.each(frappe.perm.rights, (i, key) => {
- perm[permlevel][key] = perm[permlevel][key] || (p[key] || 0);
+ if (frappe.user_roles.includes(p.role)) {
+ frappe.perm.rights.forEach(right => {
+ let value = perm[permlevel][right] || (p[right] || 0);
+ if (value) {
+ perm[permlevel][right] = value;
+ }
});
}
});
- // remove values with 0
- $.each(perm[0], (key, val) => {
- if (!val) {
- delete perm[0][key];
- }
- });
-
- $.each(perm, (i, v) => {
- if (v === undefined) {
- perm[i] = {};
- }
- });
+ // fill gaps with empty object
+ perm = perm.map(p => p || {});
+ return perm;
},
get_match_rules: (doctype, ptype) => {
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 7958ff46cc..1bec65e460 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -592,6 +592,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.render_summary(data.report_summary);
}
+ if (data.message && !data.prepared_report) this.show_status(data.message);
+
this.toggle_message(false);
if (data.result && data.result.length) {
this.prepare_report_data(data);
diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py
index 95bbae6746..d7b25b8194 100644
--- a/frappe/tests/test_safe_exec.py
+++ b/frappe/tests/test_safe_exec.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
import unittest, frappe
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import safe_exec, get_safe_globals
class TestSafeExec(unittest.TestCase):
def test_import_fails(self):
@@ -9,6 +9,15 @@ class TestSafeExec(unittest.TestCase):
def test_internal_attributes(self):
self.assertRaises(SyntaxError, safe_exec, '().__class__.__call__')
+ def test_utils(self):
+ _locals = dict(out=None)
+ safe_exec('''out = frappe.utils.cint("1")''', None, _locals)
+ self.assertEqual(_locals['out'], 1)
+
+ def test_safe_eval(self):
+ self.assertEqual(frappe.safe_eval('1+1'), 2)
+ self.assertRaises(AttributeError, frappe.safe_eval, 'frappe.utils.os.path', get_safe_globals())
+
def test_sql(self):
_locals = dict(out=None)
safe_exec('''out = frappe.db.sql("select name from tabDocType where name='DocType'")''', None, _locals)
diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py
index 40ebc8ea6e..ef572c6971 100644
--- a/frappe/tests/ui_test_helpers.py
+++ b/frappe/tests/ui_test_helpers.py
@@ -9,6 +9,9 @@ def create_if_not_exists(doc):
:param doc: dict of field value pairs. can be a list of dict for multiple records.
'''
+ if not frappe.local.dev_server:
+ frappe.throw('This method can only be accessed in development', frappe.PermissionError)
+
doc = frappe.parse_json(doc)
if not isinstance(doc, list):
diff --git a/frappe/translations/fa.csv b/frappe/translations/fa.csv
index e93fcc4a21..0698897880 100644
--- a/frappe/translations/fa.csv
+++ b/frappe/translations/fa.csv
@@ -3304,7 +3304,7 @@ Daily Long,روزانه طولانی,
Data Import Beta,واردات داده بتا,
Default Role on Creation,نقش پیش فرض در آفرینش,
Default Theme,موضوع پیش فرض,
-Default {0},پیش فرض {0,
+Default {0},پیش فرض {0},
Delete All,حذف همه,
"Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.",ترتیب اسلاید در جادوگر را تعیین می کند. اگر اسلاید نمایش داده نمی شود ، اولویت باید بر روی 0 تنظیم شود.,
Do you want to cancel all linked documents?,آیا می خواهید کلیه اسناد مرتبط را لغو کنید؟,
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index 86d7ad5a06..b8184886f9 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -29,7 +29,7 @@ class BackupGenerator:
"""
def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None,
backup_path_private_files=None, db_host="localhost", db_port=None, verbose=False,
- db_type='mariadb'):
+ db_type='mariadb', backup_path_conf=None):
global _verbose
self.db_host = db_host
self.db_port = db_port
@@ -37,8 +37,9 @@ class BackupGenerator:
self.db_type = db_type
self.user = user
self.password = password
- self.backup_path_files = backup_path_files
+ self.backup_path_conf = backup_path_conf
self.backup_path_db = backup_path_db
+ self.backup_path_files = backup_path_files
self.backup_path_private_files = backup_path_private_files
if not self.db_type:
@@ -99,11 +100,14 @@ class BackupGenerator:
def set_backup_file_name(self):
#Generate a random name using today's date and a 8 digit random number
+ for_conf = self.todays_date + "-" + self.site_slug + "-site_config_backup.json"
for_db = self.todays_date + "-" + self.site_slug + "-database.sql.gz"
for_public_files = self.todays_date + "-" + self.site_slug + "-files.tar"
for_private_files = self.todays_date + "-" + self.site_slug + "-private-files.tar"
backup_path = get_backup_path()
+ if not self.backup_path_conf:
+ self.backup_path_conf = os.path.join(backup_path, for_conf)
if not self.backup_path_db:
self.backup_path_db = os.path.join(backup_path, for_db)
if not self.backup_path_files:
@@ -166,19 +170,11 @@ class BackupGenerator:
print('Backed up files', os.path.abspath(backup_path))
def copy_site_config(self):
- site_config_backup_path = os.path.join(
- get_backup_path(),
- "{time_stamp}-{site_slug}-site_config_backup.json".format(
- time_stamp=self.todays_date,
- site_slug=self.site_slug))
+ site_config_backup_path = self.backup_path_conf
site_config_path = os.path.join(frappe.get_site_path(), "site_config.json")
- site_config = {}
- if os.path.exists(site_config_path):
- site_config.update(frappe.get_file_json(site_config_path))
- with open(site_config_backup_path, "w") as f:
- f.write(json.dumps(site_config, indent=2))
- f.flush()
- self.site_config_backup_path = site_config_backup_path
+
+ with open(site_config_backup_path, "w") as n, open(site_config_path) as c:
+ n.write(c.read())
def take_dump(self):
import frappe.utils
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index fd5c838b57..e9593ff89e 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -3,10 +3,8 @@
from __future__ import unicode_literals
-# IMPORTANT: only import safe functions as this module will be included in jinja environment
import frappe
from dateutil.parser._parser import ParserError
-import subprocess
import operator
import json
import re, datetime, math, time
@@ -427,19 +425,6 @@ def flt(s, precision=None):
return num
-def get_wkhtmltopdf_version():
- wkhtmltopdf_version = frappe.cache().hget("wkhtmltopdf_version", None)
-
- if not wkhtmltopdf_version:
- try:
- res = subprocess.check_output(["wkhtmltopdf", "--version"])
- wkhtmltopdf_version = res.decode('utf-8').split(" ")[1]
- frappe.cache().hset("wkhtmltopdf_version", None, wkhtmltopdf_version)
- except Exception:
- pass
-
- return (wkhtmltopdf_version or '0')
-
def cint(s):
"""Convert to integer"""
try: num = int(float(s))
@@ -753,7 +738,7 @@ def get_thumbnail_base64_for_image(src):
if not src:
frappe.throw('Invalid source for image: {0}'.format(src))
- if not src.startswith('/files'):
+ if not src.startswith('/files') or '..' in src:
return
def _get_base64():
@@ -1226,13 +1211,6 @@ def md_to_html(markdown_text):
return html
-def get_source_value(source, key):
- '''Get value from source (object or dict) based on key'''
- if isinstance(source, dict):
- return source.get(key)
- else:
- return getattr(source, key)
-
def is_subset(list_a, list_b):
'''Returns whether list_a is a subset of list_b'''
return len(list(set(list_a) & set(list_b))) == len(list_a)
diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py
index b1eb2b9ab3..e165a4e338 100644
--- a/frappe/utils/file_manager.py
+++ b/frappe/utils/file_manager.py
@@ -406,6 +406,10 @@ def extract_images_from_html(doc, content):
doctype = doc.parenttype if doc.parent else doc.doctype
name = doc.parent or doc.name
+ if doc.doctype == "Comment":
+ doctype = doc.reference_doctype
+ name = doc.reference_name
+
# TODO fix this
file_url = save_file(filename, content, doctype, name, decode=True).get("file_url")
if not frappe.flags.has_dataurl:
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index f3d2e75c2b..51febb5f72 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -6,6 +6,7 @@ import io
import os
import re
from distutils.version import LooseVersion
+import subprocess
import pdfkit
import six
@@ -14,7 +15,7 @@ from PyPDF2 import PdfFileReader, PdfFileWriter
import frappe
from frappe import _
-from frappe.utils import get_wkhtmltopdf_version, scrub_urls
+from frappe.utils import scrub_urls
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
@@ -191,7 +192,6 @@ def cleanup(fname, options):
if options.get(key) and os.path.exists(options[key]):
os.remove(options[key])
-
def toggle_visible_pdf(soup):
for tag in soup.find_all(attrs={"class": "visible-pdf"}):
# remove visible-pdf class to unhide
@@ -200,3 +200,16 @@ def toggle_visible_pdf(soup):
for tag in soup.find_all(attrs={"class": "hidden-pdf"}):
# remove tag from html
tag.extract()
+
+def get_wkhtmltopdf_version():
+ wkhtmltopdf_version = frappe.cache().hget("wkhtmltopdf_version", None)
+
+ if not wkhtmltopdf_version:
+ try:
+ res = subprocess.check_output(["wkhtmltopdf", "--version"])
+ wkhtmltopdf_version = res.decode('utf-8').split(" ")[1]
+ frappe.cache().hset("wkhtmltopdf_version", None, wkhtmltopdf_version)
+ except Exception:
+ pass
+
+ return (wkhtmltopdf_version or '0')
diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py
index c95b7e4699..12382e85cd 100644
--- a/frappe/utils/safe_exec.py
+++ b/frappe/utils/safe_exec.py
@@ -28,6 +28,8 @@ def safe_exec(script, _globals=None, _locals=None):
# execute script compiled by RestrictedPython
exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used
+ return exec_globals, _locals
+
def get_safe_globals():
datautils = frappe._dict()
if frappe.db:
@@ -37,7 +39,7 @@ def get_safe_globals():
date_format = "yyyy-mm-dd"
time_format = "HH:mm:ss"
- add_module_properties(frappe.utils.data, datautils, lambda obj: hasattr(obj, "__call__"))
+ add_data_utils(datautils)
if "_" in getattr(frappe.local, 'form_dict', {}):
del frappe.local.form_dict["_"]
@@ -48,9 +50,10 @@ def get_safe_globals():
# make available limited methods of frappe
json=json,
dict=dict,
+ log=frappe.log,
_dict=frappe._dict,
frappe=frappe._dict(
- flags=frappe.flags,
+ flags=frappe._dict(),
format=frappe.format_value,
format_value=frappe.format_value,
date_format=date_format,
@@ -99,7 +102,8 @@ def get_safe_globals():
scrub=scrub,
guess_mimetype=mimetypes.guess_type,
html2text=html2text,
- dev_server=1 if os.environ.get('DEV_SERVER', False) else 0
+ dev_server=1 if os.environ.get('DEV_SERVER', False) else 0,
+ run_script=run_script
)
add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception))
@@ -142,6 +146,10 @@ def read_sql(query, *args, **kwargs):
else:
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
+def run_script(script):
+ '''run another server script'''
+ return frappe.get_doc('Server Script', script).execute_method()
+
def _getitem(obj, key):
# guard function for RestrictedPython
# allow any key to be accessed as long as it does not start with underscore
@@ -154,6 +162,11 @@ def _write(obj):
# allow writing to any object
return obj
+def add_data_utils(data):
+ for key, obj in frappe.utils.data.__dict__.items():
+ if key in VALID_UTILS:
+ data[key] = obj
+
def add_module_properties(module, data, filter_method):
for key, obj in module.__dict__.items():
if key.startswith("_"):
@@ -163,3 +176,106 @@ def add_module_properties(module, data, filter_method):
if filter_method(obj):
# only allow functions
data[key] = obj
+
+VALID_UTILS = (
+"DATE_FORMAT",
+"TIME_FORMAT",
+"DATETIME_FORMAT",
+"is_invalid_date_string",
+"getdate",
+"get_datetime",
+"to_timedelta",
+"add_to_date",
+"add_days",
+"add_months",
+"add_years",
+"date_diff",
+"month_diff",
+"time_diff",
+"time_diff_in_seconds",
+"time_diff_in_hours",
+"now_datetime",
+"get_timestamp",
+"get_eta",
+"get_time_zone",
+"convert_utc_to_user_timezone",
+"now",
+"nowdate",
+"today",
+"nowtime",
+"get_first_day",
+"get_quarter_start",
+"get_first_day_of_week",
+"get_year_start",
+"get_last_day_of_week",
+"get_last_day",
+"get_time",
+"get_datetime_str",
+"get_date_str",
+"get_time_str",
+"get_user_date_format",
+"get_user_time_format",
+"format_date",
+"format_time",
+"format_datetime",
+"format_duration",
+"get_weekdays",
+"get_weekday",
+"get_timespan_date_range",
+"global_date_format",
+"has_common",
+"flt",
+"cint",
+"floor",
+"ceil",
+"cstr",
+"rounded",
+"remainder",
+"safe_div",
+"round_based_on_smallest_currency_fraction",
+"encode",
+"parse_val",
+"fmt_money",
+"get_number_format_info",
+"money_in_words",
+"in_words",
+"is_html",
+"is_image",
+"get_thumbnail_base64_for_image",
+"image_to_base64",
+"strip_html",
+"escape_html",
+"pretty_date",
+"comma_or",
+"comma_and",
+"comma_sep",
+"new_line_sep",
+"filter_strip_join",
+"get_url",
+"get_host_name_from_request",
+"url_contains_port",
+"get_host_name",
+"get_link_to_form",
+"get_link_to_report",
+"get_absolute_url",
+"get_url_to_form",
+"get_url_to_list",
+"get_url_to_report",
+"get_url_to_report_with_filters",
+"evaluate_filters",
+"compare",
+"get_filter",
+"make_filter_tuple",
+"make_filter_dict",
+"sanitize_column",
+"scrub_urls",
+"expand_relative_urls",
+"quoted",
+"quote_urls",
+"unique",
+"strip",
+"to_markdown",
+"md_to_html",
+"is_subset",
+"generate_hash"
+)
\ No newline at end of file
diff --git a/frappe/website/doctype/blog_post/blog_post.js b/frappe/website/doctype/blog_post/blog_post.js
index 7aa83f536d..bfff947948 100644
--- a/frappe/website/doctype/blog_post/blog_post.js
+++ b/frappe/website/doctype/blog_post/blog_post.js
@@ -11,16 +11,29 @@ frappe.ui.form.on('Blog Post', {
},
title: function(frm) {
generate_google_search_preview(frm);
+ frm.trigger('set_route');
},
meta_description: function(frm) {
generate_google_search_preview(frm);
},
blog_intro: function(frm) {
generate_google_search_preview(frm);
+ },
+ blog_category(frm) {
+ frm.trigger('set_route');
+ },
+ set_route(frm) {
+ if (frm.doc.route) return;
+ if (frm.doc.title && frm.doc.blog_category) {
+ frm.call('make_route').then(r => {
+ frm.set_value('route', r.message);
+ });
+ }
}
});
function generate_google_search_preview(frm) {
+ if (!frm.doc.title) return;
let google_preview = frm.get_field("google_preview");
let seo_title = (frm.doc.title).slice(0, 60);
let seo_description = (frm.doc.meta_description || frm.doc.blog_intro || "").slice(0, 160);
diff --git a/frappe/website/web_template/section_with_tabs/section_with_tabs.html b/frappe/website/web_template/section_with_tabs/section_with_tabs.html
index 0c206b1c48..9a5bb20e0f 100644
--- a/frappe/website/web_template/section_with_tabs/section_with_tabs.html
+++ b/frappe/website/web_template/section_with_tabs/section_with_tabs.html
@@ -1,5 +1,8 @@
{{ title }}
+
+{%- if subtitle -%}
{{ subtitle }}
+{%- endif -%}
{% set ns = namespace(tabs=[]) %}
diff --git a/package.json b/package.json
index f893d03ad3..fb3e507bd5 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"dependencies": {
"ace-builds": "^1.4.8",
"air-datepicker": "http://github.com/frappe/air-datepicker",
+ "autoprefixer": "^9.8.6",
"awesomplete": "^1.1.5",
"bootstrap": "^4.4.1",
"cookie": "^0.4.0",
diff --git a/rollup/config.js b/rollup/config.js
index 460780bc1b..b1816cb4c6 100644
--- a/rollup/config.js
+++ b/rollup/config.js
@@ -117,6 +117,7 @@ function get_rollup_options_for_css(output_file, input_files) {
// less -> css
postcss({
plugins: [
+ starts_with_css ? require('autoprefixer')() : null,
starts_with_css && production ? require('cssnano')({ preset: 'default' }) : null
].filter(Boolean),
extract: output_path,
diff --git a/yarn.lock b/yarn.lock
index c3808f680a..23bfe25255 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -836,6 +836,19 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+autoprefixer@^9.8.6:
+ version "9.8.6"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
+ integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==
+ dependencies:
+ browserslist "^4.12.0"
+ caniuse-lite "^1.0.30001109"
+ colorette "^1.2.1"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^7.0.32"
+ postcss-value-parser "^4.1.0"
+
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
@@ -924,9 +937,9 @@ big.js@^3.1.3:
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
bl@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
- integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.1.tgz#1cbb439299609e419b5a74d7fce2f8b37d8e5c6f"
+ integrity sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ==
dependencies:
readable-stream "^3.0.1"
@@ -1048,6 +1061,16 @@ browserslist@^4.0.0:
electron-to-chromium "^1.3.113"
node-releases "^1.1.8"
+browserslist@^4.12.0:
+ version "4.14.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.0.tgz#2908951abfe4ec98737b72f34c3bcedc8d43b000"
+ integrity sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==
+ dependencies:
+ caniuse-lite "^1.0.30001111"
+ electron-to-chromium "^1.3.523"
+ escalade "^3.0.2"
+ node-releases "^1.1.60"
+
buble@^0.19.6:
version "0.19.6"
resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3"
@@ -1203,6 +1226,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz"
integrity sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==
+caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
+ version "1.0.30001118"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz#116a9a670e5264aec895207f5e918129174c6f62"
+ integrity sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg==
+
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -1427,6 +1455,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
+colorette@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
+ integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
+
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -2134,6 +2167,11 @@ electron-to-chromium@^1.3.113:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9"
integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==
+electron-to-chromium@^1.3.523:
+ version "1.3.551"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.551.tgz#a94d243a4ca90705189bd4a5eca4e0f56b745a4f"
+ integrity sha512-11qcm2xvf2kqeFO5EIejaBx5cKXsW1quAyv3VctCMYwofnyVZLs97y6LCekss3/ghQpr7PYkSO3uId5FmxZsdw==
+
elegant-spinner@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
@@ -2309,6 +2347,11 @@ es6-promisify@^5.0.0:
dependencies:
es6-promise "^4.0.3"
+escalade@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4"
+ integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==
+
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
@@ -3922,7 +3965,7 @@ js-tokens@^4.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-yaml@^3.10.0:
+js-yaml@^3.10.0, js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@^3.9.0:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
@@ -3930,22 +3973,6 @@ js-yaml@^3.10.0:
argparse "^1.0.7"
esprima "^4.0.0"
-js-yaml@^3.12.0, js-yaml@^3.9.0:
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc"
- integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==
- dependencies:
- argparse "^1.0.7"
- esprima "^4.0.0"
-
-js-yaml@^3.13.1:
- version "3.13.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
- integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
- dependencies:
- argparse "^1.0.7"
- esprima "^4.0.0"
-
jsbarcode@^3.9.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/jsbarcode/-/jsbarcode-3.11.0.tgz#20623e008b101ef45d0cce9c8022cdf49be28547"
@@ -4816,6 +4843,11 @@ node-gyp@^3.8.0:
tar "^2.0.0"
which "1"
+node-releases@^1.1.60:
+ version "1.1.60"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
+ integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==
+
node-releases@^1.1.8:
version "1.1.9"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.9.tgz#70d0985ec4bf7de9f08fc481f5dae111889ca482"
@@ -4878,6 +4910,11 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+ integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
normalize-url@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
@@ -4912,6 +4949,11 @@ nth-check@^1.0.2:
dependencies:
boolbase "~1.0.0"
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+ integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -5731,6 +5773,11 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.1:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+postcss-value-parser@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+ integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
postcss@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.1.tgz#000dbd1f8eef217aa368b9a212c5fc40b2a8f3f2"
@@ -5768,6 +5815,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.5:
source-map "^0.6.1"
supports-color "^6.1.0"
+postcss@^7.0.32:
+ version "7.0.32"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
+ integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
+ dependencies:
+ chalk "^2.4.2"
+ source-map "^0.6.1"
+ supports-color "^6.1.0"
+
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -6085,7 +6141,7 @@ readable-stream@1.1.x:
isarray "0.0.1"
string_decoder "~0.10.x"
-readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.2.2, readable-stream@~2.3.6:
+readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.5, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -6098,19 +6154,6 @@ readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.2.2, readable-stre
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.3.5:
- version "2.3.6"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
- integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.3"
- isarray "~1.0.0"
- process-nextick-args "~2.0.0"
- safe-buffer "~5.1.1"
- string_decoder "~1.1.1"
- util-deprecate "~1.0.1"
-
readable-stream@^3.0.1, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
@@ -6540,11 +6583,16 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+safe-buffer@^5.0.1, safe-buffer@^5.1.2:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"