Переглянути джерело

Merge branch 'develop' into fix-notification-links

version-14
Suraj Shetty 4 роки тому
committed by GitHub
джерело
коміт
0b1c248a7c
Не вдалося знайти GPG ключ що відповідає даному підпису Ідентифікатор GPG ключа: 4AEE18F83AFDEB23
97 змінених файлів з 1893 додано та 1927 видалено
  1. +21
    -15
      .github/frappe_linter/translation.py
  2. +3
    -2
      frappe/__init__.py
  3. +1
    -0
      frappe/automation/doctype/auto_repeat/auto_repeat.py
  4. +49
    -596
      frappe/core/doctype/activity_log/activity_log.json
  5. +0
    -3
      frappe/core/doctype/activity_log/activity_log.py
  6. +3
    -1
      frappe/core/doctype/docfield/docfield.json
  7. +14
    -5
      frappe/core/doctype/doctype_action/doctype_action.json
  8. +24
    -20
      frappe/core/doctype/file/file.py
  9. +46
    -26
      frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
  10. +10
    -5
      frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
  11. +4
    -3
      frappe/core/doctype/server_script/server_script.json
  12. +2
    -1
      frappe/core/doctype/server_script/server_script.py
  13. +12
    -0
      frappe/core/doctype/server_script/test_server_script.py
  14. +3
    -9
      frappe/core/doctype/user/user.json
  15. +273
    -271
      frappe/custom/doctype/custom_field/custom_field.json
  16. +78
    -174
      frappe/custom/doctype/custom_script/custom_script.json
  17. +257
    -255
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  18. +1
    -0
      frappe/custom/doctype/package_publish_tool/package_publish_tool.py
  19. +10
    -3
      frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
  20. +2
    -1
      frappe/data_migration/doctype/data_migration_run/data_migration_run.py
  21. +1
    -1
      frappe/database/mariadb/framework_mariadb.sql
  22. +2
    -0
      frappe/database/mariadb/setup_db.py
  23. +1
    -1
      frappe/database/postgres/framework_postgres.sql
  24. +1
    -1
      frappe/database/postgres/schema.py
  25. +14
    -1
      frappe/desk/desktop.py
  26. +0
    -0
      frappe/desk/doctype/console_log/__init__.py
  27. +8
    -0
      frappe/desk/doctype/console_log/console_log.js
  28. +52
    -0
      frappe/desk/doctype/console_log/console_log.json
  29. +10
    -0
      frappe/desk/doctype/console_log/console_log.py
  30. +10
    -0
      frappe/desk/doctype/console_log/test_console_log.py
  31. +0
    -0
      frappe/desk/doctype/system_console/__init__.py
  32. +21
    -0
      frappe/desk/doctype/system_console/system_console.js
  33. +68
    -0
      frappe/desk/doctype/system_console/system_console.json
  34. +38
    -0
      frappe/desk/doctype/system_console/system_console.py
  35. +20
    -0
      frappe/desk/doctype/system_console/test_system_console.py
  36. +1
    -1
      frappe/desk/form/meta.py
  37. +5
    -3
      frappe/desk/link_preview.py
  38. +0
    -263
      frappe/desk/query_builder.py
  39. +3
    -0
      frappe/desk/query_report.py
  40. +1
    -0
      frappe/desk/reportview.py
  41. +2
    -3
      frappe/desk/treeview.py
  42. +0
    -3
      frappe/email/doctype/newsletter/newsletter.js
  43. +39
    -9
      frappe/email/doctype/newsletter/newsletter.json
  44. +19
    -21
      frappe/email/doctype/newsletter/newsletter.py
  45. +4
    -2
      frappe/email/doctype/newsletter/newsletter_list.js
  46. +1
    -0
      frappe/email/doctype/newsletter/test_newsletter.py
  47. +20
    -5
      frappe/email/doctype/notification/notification.js
  48. +10
    -1
      frappe/email/doctype/notification/notification.json
  49. +28
    -7
      frappe/email/doctype/notification/notification.py
  50. +88
    -1
      frappe/email/doctype/notification/test_notification.py
  51. +2
    -1
      frappe/email/doctype/notification_recipient/notification_recipient.json
  52. +1
    -1
      frappe/email/email_body.py
  53. +1
    -1
      frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
  54. +6
    -13
      frappe/event_streaming/doctype/event_consumer/event_consumer.json
  55. +23
    -14
      frappe/event_streaming/doctype/event_consumer/event_consumer.py
  56. +7
    -4
      frappe/event_streaming/doctype/event_producer/event_producer.json
  57. +31
    -11
      frappe/event_streaming/doctype/event_producer/event_producer.py
  58. +34
    -14
      frappe/event_streaming/doctype/event_producer/test_event_producer.py
  59. +4
    -1
      frappe/installer.py
  60. +6
    -0
      frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
  61. +2
    -2
      frappe/integrations/doctype/webhook/webhook.py
  62. +2
    -1
      frappe/model/__init__.py
  63. +1
    -0
      frappe/model/db_query.py
  64. +2
    -2
      frappe/model/document.py
  65. +2
    -0
      frappe/model/naming.py
  66. +0
    -1
      frappe/model/rename_doc.py
  67. +4
    -0
      frappe/patches.txt
  68. +44
    -0
      frappe/patches/v12_0/fix_email_id_formatting.py
  69. +11
    -0
      frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
  70. +13
    -0
      frappe/patches/v13_0/enable_custom_script.py
  71. +12
    -0
      frappe/patches/v13_0/update_newsletter_content_type.py
  72. +6
    -3
      frappe/printing/doctype/print_format/print_format.json
  73. +1
    -1
      frappe/public/build.json
  74. +1
    -1
      frappe/public/js/frappe/form/controls/data.js
  75. +44
    -17
      frappe/public/js/frappe/form/form.js
  76. +19
    -24
      frappe/public/js/frappe/model/perm.js
  77. +17
    -13
      frappe/public/js/frappe/ui/tree.js
  78. +3
    -1
      frappe/public/js/frappe/views/reports/query_report.js
  79. +15
    -2
      frappe/public/js/frappe/views/treeview.js
  80. +10
    -1
      frappe/tests/test_safe_exec.py
  81. +3
    -0
      frappe/tests/ui_test_helpers.py
  82. +1
    -1
      frappe/translations/fa.csv
  83. +2
    -1
      frappe/utils/__init__.py
  84. +24
    -15
      frappe/utils/backups.py
  85. +1
    -23
      frappe/utils/data.py
  86. +4
    -0
      frappe/utils/file_manager.py
  87. +15
    -2
      frappe/utils/pdf.py
  88. +119
    -3
      frappe/utils/safe_exec.py
  89. +14
    -1
      frappe/website/doctype/blog_post/blog_post.js
  90. +8
    -1
      frappe/website/doctype/blog_post/blog_post.json
  91. +6
    -1
      frappe/website/doctype/blog_post/blog_post.py
  92. +2
    -1
      frappe/website/doctype/web_page/web_page.json
  93. +3
    -0
      frappe/website/web_template/section_with_tabs/section_with_tabs.html
  94. +1
    -0
      package.json
  95. +2
    -1
      requirements.txt
  96. +1
    -0
      rollup/config.js
  97. +83
    -35
      yarn.lock

+ 21
- 15
.github/frappe_linter/translation.py Переглянути файл

@@ -7,22 +7,28 @@ start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")

# skip first argument
files = sys.argv[1:]
for _file in files:
if not _file.endswith(('.py', '.js')):
continue
files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))]
for _file in files_to_scan:
with open(_file, 'r') as f:
print(f'Checking: {_file}')
for num, line in enumerate(f, 1):
all_matches = start_pattern.finditer(line)
if all_matches:
for match in all_matches:
verify = pattern.search(line)
if not verify:
errors_encounter += 1
print(f'A syntax error has been discovered at line number: {num}')
print(f'Syntax error occurred with: {line}')
file_lines = f.readlines()
for line_number, line in enumerate(file_lines, 1):
start_matches = start_pattern.search(line)
if start_matches:
match = pattern.search(line)
if not match and line.endswith(',\n'):
# concat remaining text to validate multiline pattern
line = "".join(file_lines[line_number - 1:])
line = line[start_matches.start() + 1:]
match = pattern.match(line)

if not match:
errors_encounter += 1
print(f'\nTranslation syntax error at line number: {line_number + 1}\n{line.strip()[:100]}')

if errors_encounter > 0:
print('You can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
assert 1+1 == 3
print('\nYou can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
sys.exit(1)
else:
print('Good To Go!')
print('\nGood To Go!')

+ 3
- 2
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()

@@ -1109,8 +1110,8 @@ def get_newargs(fn, kwargs):
if (a in fnargs) or varkw:
newargs[a] = kwargs.get(a)

if "flags" in newargs:
del newargs["flags"]
newargs.pop("ignore_permissions", None)
newargs.pop("flags", None)

return newargs



+ 1
- 0
frappe/automation/doctype/auto_repeat/auto_repeat.py Переглянути файл

@@ -403,6 +403,7 @@ def update_reference(docname, reference):

@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
frappe.has_permission("Auto Repeat", "write", throw=True)
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})


+ 49
- 596
frappe/core/doctype/activity_log/activity_log.json Переглянути файл

@@ -1,731 +1,184 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
{
"actions": [],
"allow_import": 1,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2017-10-05 11:10:38.780133",
"custom": 0,
"description": "Keep track of all update feeds",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"subject",
"section_break_8",
"content",
"column_break_5",
"additional_info",
"communication_date",
"column_break_7",
"operation",
"status",
"reference_section",
"reference_doctype",
"reference_name",
"reference_owner",
"column_break_14",
"timeline_doctype",
"timeline_name",
"link_doctype",
"link_name",
"user",
"full_name"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "subject",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8",
"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,
"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
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"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": "Message",
"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,
"width": "400"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5",
"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,
"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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "additional_info",
"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": "More Information",
"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
"label": "More Information"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Now",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"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": "Date",
"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
"label": "Date"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"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,
"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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "operation",
"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": "Operation",
"length": 0,
"no_copy": 0,
"options": "\nLogin\nLogout",
"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
"options": "\nLogin\nLogout"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status",
"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": 0,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nSuccess\nFailed\nLinked\nClosed",
"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
"options": "\nSuccess\nFailed\nLinked\nClosed"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "reference_section",
"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": "Reference",
"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
"label": "Reference"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_doctype",
"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": "Reference Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"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
"options": "DocType"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic 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": "Reference Name",
"length": 0,
"no_copy": 0,
"options": "reference_doctype",
"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
"options": "reference_doctype"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "reference_name.owner",
"fieldname": "reference_owner",
"fieldtype": "Read Only",
"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 Owner",
"length": 0,
"no_copy": 0,
"options": "",
"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": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_14",
"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,
"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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "timeline_doctype",
"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": "Timeline DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"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
"options": "DocType"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "timeline_name",
"fieldtype": "Dynamic 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": "Timeline Name",
"length": 0,
"no_copy": 0,
"options": "timeline_doctype",
"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
"options": "timeline_doctype"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_doctype",
"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": "Link DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"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
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_name",
"fieldtype": "Dynamic 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": "Link Name",
"length": 0,
"no_copy": 0,
"options": "link_doctype",
"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
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"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
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Full Name",
"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
"label": "Full Name"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-comment",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-09-05 14:22:27.664645",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-28 11:43:57.504565",
"modified_by": "Administrator",
"module": "Core",
"name": "Activity Log",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
"share": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"share": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "subject",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",


+ 0
- 3
frappe/core/doctype/activity_log/activity_log.py Переглянути файл

@@ -25,9 +25,6 @@ class ActivityLog(Document):
if self.reference_doctype and self.reference_name:
self.status = "Linked"

def on_trash(self): # pylint: disable=no-self-use
frappe.throw(_("Sorry! You cannot delete auto-generated comments"))

def on_doctype_update():
"""Add indexes in `tabActivity Log`"""
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"])


+ 3
- 1
frappe/core/doctype/docfield/docfield.json Переглянути файл

@@ -163,6 +163,7 @@
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
@@ -475,9 +476,10 @@
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-02-06 09:06:25.224413",
"modified": "2020-08-28 11:28:21.252853",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",


+ 14
- 5
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",


+ 24
- 20
frappe/core/doctype/file/file.py Переглянути файл

@@ -278,25 +278,26 @@ class File(Document):
base_url = os.path.dirname(self.file_url)

files = []
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(os.path.dirname(zip_path))
for info in zf.infolist():
if not info.filename.startswith('__MACOSX'):
file_url = file_url = base_url + '/' + info.filename
file_name = frappe.db.get_value('File', dict(file_url=file_url))
if file_name:
file_doc = frappe.get_doc('File', file_name)
else:
file_doc = frappe.new_doc("File")
file_doc.file_name = info.filename
file_doc.file_size = info.file_size
file_doc.folder = self.folder
file_doc.is_private = self.is_private
file_doc.file_url = file_url
file_doc.attached_to_doctype = self.attached_to_doctype
file_doc.attached_to_name = self.attached_to_name
file_doc.save()
files.append(file_doc)
with zipfile.ZipFile(zip_path) as z:
for file in z.filelist:
if file.is_dir() or file.filename.startswith('__MACOSX/'):
# skip directories and macos hidden directory
continue

filename = os.path.basename(file.filename)
if filename.startswith('.'):
# skip hidden files
continue

file_doc = frappe.new_doc('File')
file_doc.content = z.read(file.filename)
file_doc.file_name = filename
file_doc.folder = self.folder
file_doc.is_private = self.is_private
file_doc.attached_to_doctype = self.attached_to_doctype
file_doc.attached_to_name = self.attached_to_name
file_doc.save()
files.append(file_doc)

frappe.delete_doc('File', self.name)
return files
@@ -359,6 +360,9 @@ class File(Document):
"""write file to disk with a random name (to compare)"""
file_path = get_files_path(is_private=self.is_private)

if os.path.sep in self.file_name:
frappe.throw(_('File name cannot have {0}').format(os.path.sep))

# create directory (if not exists)
frappe.create_folder(file_path)
# write the file
@@ -938,7 +942,7 @@ def attach_files_to_document(doc, event):
# we dont want the update to fail if file cannot be attached for some reason
try:
value = doc.get(df.fieldname)
if not value.startswith(("/files", "/private/files")):
if not (value or '').startswith(("/files", "/private/files")):
return

if frappe.db.exists("File", {


+ 46
- 26
frappe/core/doctype/scheduled_job_type/scheduled_job_type.py Переглянути файл

@@ -109,12 +109,14 @@ class ScheduledJobType(Document):
def on_trash(self):
frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)


@frappe.whitelist()
def execute_event(doc):
frappe.only_for('System Manager')
doc = json.loads(doc)
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue()


def run_scheduled_job(job_type):
'''This is a wrapper function that runs a hooks.scheduler_events method'''
try:
@@ -122,44 +124,62 @@ def run_scheduled_job(job_type):
except Exception:
print(frappe.get_traceback())

def sync_jobs():
frappe.reload_doc('core', 'doctype', 'scheduled_job_type')
all_events = []
scheduler_events = frappe.get_hooks("scheduler_events")
insert_events(all_events, scheduler_events)
clear_events(all_events, scheduler_events)

def insert_events(all_events, scheduler_events):
def sync_jobs(hooks=None):
frappe.reload_doc("core", "doctype", "scheduled_job_type")
scheduler_events = hooks or frappe.get_hooks("scheduler_events")
all_events = insert_events(scheduler_events)
clear_events(all_events)


def insert_events(scheduler_events):
cron_jobs, event_jobs = [], []
for event_type in scheduler_events:
events = scheduler_events.get(event_type)
if isinstance(events, dict):
insert_cron_event(events, all_events)
cron_jobs += insert_cron_jobs(events)
else:
# hourly, daily etc
insert_event_list(events, event_type, all_events)
event_jobs += insert_event_jobs(events, event_type)
return cron_jobs + event_jobs


def insert_cron_event(events, all_events):
def insert_cron_jobs(events):
cron_jobs = []
for cron_format in events:
for event in events.get(cron_format):
all_events.append(event)
insert_single_event('Cron', event, cron_format)
cron_jobs.append(event)
insert_single_event("Cron", event, cron_format)
return cron_jobs


def insert_event_list(events, event_type, all_events):
def insert_event_jobs(events, event_type):
event_jobs = []
for event in events:
all_events.append(event)
event_jobs.append(event)
frequency = event_type.replace('_', ' ').title()
insert_single_event(frequency, event)
return event_jobs


def insert_single_event(frequency, event, cron_format=None):
cron_expr = {"cron_format": cron_format} if cron_format else {}
doc = frappe.get_doc({
"doctype": "Scheduled Job Type",
"method": event,
"cron_format": cron_format,
"frequency": frequency
})

if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }):
try:
doc.insert()
except frappe.DuplicateEntryError:
doc.delete()
doc.insert()


def insert_single_event(frequency, event, cron_format = None):
if not frappe.db.exists('Scheduled Job Type', dict(method=event)):
frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = event,
cron_format = cron_format,
frequency = frequency
)).insert()

def clear_events(all_events, scheduler_events):
for event in frappe.get_all('Scheduled Job Type', ('name', 'method')):
def clear_events(all_events):
for event in frappe.get_all("Scheduled Job Type", ("name", "method")):
if event.method not in all_events:
frappe.delete_doc('Scheduled Job Type', event.name)
frappe.delete_doc("Scheduled Job Type", event.name)

+ 10
- 5
frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py Переглянути файл

@@ -11,11 +11,10 @@ from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs

class TestScheduledJobType(unittest.TestCase):
def setUp(self):
if not frappe.get_all('Scheduled Job Type', limit=1):
frappe.db.rollback()
frappe.db.sql('truncate `tabScheduled Job Type`')
sync_jobs()
frappe.db.commit()
frappe.db.rollback()
frappe.db.sql('truncate `tabScheduled Job Type`')
sync_jobs()
frappe.db.commit()

def test_sync_jobs(self):
all_job = frappe.get_doc('Scheduled Job Type',
@@ -32,6 +31,12 @@ class TestScheduledJobType(unittest.TestCase):
self.assertEqual(cron_job.frequency, 'Cron')
self.assertEqual(cron_job.cron_format, '0/15 * * * *')

# check if jobs are synced after change in hooks
updated_scheduler_events = { "hourly": ["frappe.email.queue.flush"] }
sync_jobs(updated_scheduler_events)
updated_scheduled_job = frappe.get_doc("Scheduled Job Type", {"method": "frappe.email.queue.flush"})
self.assertEqual(updated_scheduled_job.frequency, "Hourly")

def test_daily_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox'))
job.db_set('last_execution', '2019-01-01 00:00:00')


+ 4
- 3
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",


+ 2
- 1
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


+ 12
- 0
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')

+ 3
- 9
frappe/core/doctype/user/user.json Переглянути файл

@@ -356,7 +356,7 @@
"depends_on": "enabled",
"fieldname": "email_settings",
"fieldtype": "Section Break",
"label": "Email Settings"
"label": "Email"
},
{
"default": "1",
@@ -382,12 +382,6 @@
"label": "Email Signature",
"no_copy": 1
},
{
"collapsible": 1,
"fieldname": "email_inbox",
"fieldtype": "Section Break",
"label": "Email Inbox"
},
{
"fieldname": "user_emails",
"fieldtype": "Table",
@@ -651,7 +645,7 @@
}
],
"max_attachments": 5,
"modified": "2020-08-06 19:48:49.677800",
"modified": "2020-08-26 19:48:49.677800",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -685,4 +679,4 @@
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
}
}

+ 273
- 271
frappe/custom/doctype/custom_field/custom_field.json Переглянути файл

@@ -58,382 +58,384 @@
],
"fields": [
{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
"default": "0",
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-02-06 23:43:00.123575",
"modified": "2020-08-28 11:28:44.377753",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"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": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",


+ 78
- 174
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": "<h3>Custom Script Help</h3>\n<p>Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>\n<pre><code>\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 &lt; 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' &amp;&amp; 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 &gt; 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</code></pre>",
"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": "<h3>Custom Script Help</h3>\n<p>Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>\n<pre><code>\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 &lt; 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' &amp;&amp; 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 &gt; 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</code></pre>",
"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
}

+ 257
- 255
frappe/custom/doctype/customize_form_field/customize_form_field.json Переглянути файл

@@ -60,364 +60,366 @@
],
"fields": [
{
"fieldname": "label_and_type",
"fieldtype": "Section Break",
"label": "Label and Type"
"fieldname": "label_and_type",
"fieldtype": "Section Break",
"label": "Label and Type"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"oldfieldname": "label",
"oldfieldtype": "Data",
"search_index": 1
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"oldfieldname": "label",
"oldfieldtype": "Data",
"search_index": 1
},
{
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name",
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1,
"search_index": 1
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name",
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory",
"oldfieldname": "reqd",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
"default": "0",
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory",
"oldfieldname": "reqd",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
},
{
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age&gt;18",
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"oldfieldname": "depends_on",
"oldfieldtype": "Data",
"options": "JS"
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age&gt;18",
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"oldfieldname": "depends_on",
"oldfieldtype": "Data",
"options": "JS"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Perm Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Perm Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden",
"oldfieldname": "hidden",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden",
"oldfieldname": "hidden",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype == \"Table\"",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
"default": "0",
"depends_on": "eval: doc.fieldtype == \"Table\"",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On",
"options": "JS"
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On",
"options": "JS"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
"default": "0",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:(doc.fieldtype == 'Link')",
"fieldname": "remember_last_selected_value",
"fieldtype": "Check",
"label": "Remember Last Selected Value"
"default": "0",
"depends_on": "eval:(doc.fieldtype == 'Link')",
"fieldname": "remember_last_selected_value",
"fieldtype": "Check",
"label": "Remember Last Selected Value"
},
{
"fieldname": "display",
"fieldtype": "Section Break",
"label": "Display"
"fieldname": "display",
"fieldtype": "Section Break",
"label": "Display"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default",
"oldfieldname": "default",
"oldfieldtype": "Text"
"fieldname": "default",
"fieldtype": "Text",
"label": "Default",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"default": "0",
"fieldname": "in_filter",
"fieldtype": "Check",
"label": "In Filter",
"oldfieldname": "in_filter",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
"default": "0",
"fieldname": "in_filter",
"fieldtype": "Check",
"label": "In Filter",
"oldfieldname": "in_filter",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"description": "Print Width of the field, if the field is a column in a table",
"fieldname": "print_width",
"fieldtype": "Data",
"label": "Print Width",
"print_width": "50px",
"width": "50px"
"description": "Print Width of the field, if the field is a column in a table",
"fieldname": "print_width",
"fieldtype": "Data",
"label": "Print Width",
"print_width": "50px",
"width": "50px"
},
{
"depends_on": "eval:cur_frm.doc.istable",
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
"depends_on": "eval:cur_frm.doc.istable",
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "width",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data",
"print_width": "50px",
"width": "50px"
"fieldname": "width",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "is_custom_field",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Custom Field",
"read_only": 1
"default": "0",
"fieldname": "is_custom_field",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Custom Field",
"read_only": 1
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"fieldname": "property_depends_on_section",
"fieldtype": "Section Break",
"label": "Property Depends On"
"fieldname": "property_depends_on_section",
"fieldtype": "Section Break",
"label": "Property Depends On"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"options": "JS"
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"options": "JS"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-06-02 23:45:46.810868",
"modified": "2020-08-28 11:28:59.084060",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",


+ 1
- 0
frappe/custom/doctype/package_publish_tool/package_publish_tool.py Переглянути файл

@@ -100,6 +100,7 @@ def export_package():
@frappe.whitelist()
def import_package(package=None):
"""Import package from JSON."""
frappe.only_for("System Manager")
if isinstance(package, string_types):
package = json.loads(package)



+ 10
- 3
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)

+ 2
- 1
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):


+ 1
- 1
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`)


+ 2
- 0
frappe/database/mariadb/setup_db.py Переглянути файл

@@ -92,6 +92,8 @@ def bootstrap_database(db_name, verbose, source_sql=None):
sys.exit(1)

import_db_from_sql(source_sql, verbose)

frappe.connect(db_name=db_name)
if not 'tabDefaultValue' in frappe.db.get_tables():
print('''Database not installed, this can due to lack of permission, or that the database name exists.
Check your mysql root password, or use --force to reinstall''')


+ 1
- 1
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")


+ 1
- 1
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)


+ 14
- 1
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())

+ 0
- 0
frappe/desk/doctype/console_log/__init__.py Переглянути файл


+ 8
- 0
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) {

// }
});

+ 52
- 0
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
}

+ 10
- 0
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

+ 10
- 0
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

+ 0
- 0
frappe/desk/doctype/system_console/__init__.py Переглянути файл


+ 21
- 0
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');
});
}
});

+ 68
- 0
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 <code>log(text)</code>",
"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
}

+ 38
- 0
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()

+ 20
- 0
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')

+ 1
- 1
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)



+ 5
- 3
frappe/desk/link_preview.py Переглянути файл

@@ -1,5 +1,5 @@
import frappe
from frappe.model import no_value_fields
from frappe.model import no_value_fields, table_fields
import json

@frappe.whitelist()
@@ -9,11 +9,13 @@ def get_preview_data(doctype, docname):
if not meta.show_preview_popup: return

preview_fields = [field.fieldname for field in meta.fields \
if field.in_preview and field.fieldtype not in no_value_fields]
if field.in_preview and field.fieldtype not in no_value_fields \
and field.fieldtype not in table_fields]

# no preview fields defined, build list from mandatory fields
if not preview_fields:
preview_fields = [field.fieldname for field in meta.fields if field.reqd]
preview_fields = [field.fieldname for field in meta.fields if field.reqd \
and field.fieldtype not in table_fields]

title_field = meta.get_title_field()
image_field = meta.image_field


+ 0
- 263
frappe/desk/query_builder.py Переглянути файл

@@ -1,263 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe

out = frappe.response

from frappe.utils import cint
import frappe.defaults
from six import text_type

def get_sql_tables(q):
if q.find('WHERE') != -1:
tl = q.split('FROM')[1].split('WHERE')[0].split(',')
elif q.find('GROUP BY') != -1:
tl = q.split('FROM')[1].split('GROUP BY')[0].split(',')
else:
tl = q.split('FROM')[1].split('ORDER BY')[0].split(',')
return [t.strip().strip('`')[3:] for t in tl]

def get_parent_dt(dt):
pdt = ''
if frappe.db.sql('select name from `tabDocType` where istable=1 and name=%s', dt):
import frappe.model.meta
return frappe.model.meta.get_parent_dt(dt)
return pdt

def get_sql_meta(tl):
std_columns = {
'owner':('Owner', '', '', '100'),
'creation':('Created on', 'Date', '', '100'),
'modified':('Last modified on', 'Date', '', '100'),
'modified_by':('Modified By', '', '', '100')
}

meta = {}

for dt in tl:
meta[dt] = std_columns.copy()

# for table doctype, the ID is the parent id
pdt = get_parent_dt(dt)
if pdt:
meta[dt]['parent'] = ('ID', 'Link', pdt, '200')

# get the field properties from DocField
res = frappe.db.sql("select fieldname, label, fieldtype, options, width \
from tabDocField where parent=%s", dt)
for r in res:
if r[0]:
meta[dt][r[0]] = (r[1], r[2], r[3], r[4]);

# name
meta[dt]['name'] = ('ID', 'Link', dt, '200')

return meta

def add_match_conditions(q, tl):
from frappe.desk.reportview import build_match_conditions
sl = []
for dt in tl:
s = build_match_conditions(dt)
if s:
sl.append(s)

# insert the conditions
if sl:
condition_st = q.find('WHERE')!=-1 and ' AND ' or ' WHERE '
condition_end = q.find('ORDER BY')!=-1 and 'ORDER BY' or 'LIMIT'
condition_end = q.find('GROUP BY')!=-1 and 'GROUP BY' or condition_end

if q.find('ORDER BY')!=-1 or q.find('LIMIT')!=-1 or q.find('GROUP BY')!=-1: # if query continues beyond conditions
q = q.split(condition_end)
q = q[0] + condition_st + '(' + ' OR '.join(sl) + ') ' + condition_end + q[1]
else:
q = q + condition_st + '(' + ' OR '.join(sl) + ')'

return q

def guess_type(m):
"""
Returns fieldtype depending on the MySQLdb Description
"""
if frappe.db.is_type_number(m):
return 'Currency'
elif m in frappe.is_type_datetime(m):
return 'Date'
else:
return 'Data'

def build_description_simple():
colnames, coltypes, coloptions, colwidths = [], [], [], []

for m in frappe.db.get_description():
colnames.append(m[0])
coltypes.append(guess_type[m[1]])
coloptions.append('')
colwidths.append('100')

return colnames, coltypes, coloptions, colwidths

def build_description_standard(meta, tl):

desc = frappe.db.get_description()

colnames, coltypes, coloptions, colwidths = [], [], [], []

# merged metadata - used if we are unable to
# get both the table name and field name from
# the description - in case of joins
merged_meta = {}
for d in meta:
merged_meta.update(meta[d])

for f in desc:
fn, dt = f[0], ''
if '.' in fn:
dt, fn = fn.split('.')

if (not dt) and merged_meta.get(fn):
# no "AS" given, find type from merged description

desc = merged_meta[fn]
colnames.append(desc[0] or fn)
coltypes.append(desc[1] or '')
coloptions.append(desc[2] or '')
colwidths.append(desc[3] or '100')

elif fn in meta.get(dt,{}):
# type specified for a multi-table join
# usually from Report Builder

desc = meta[dt][fn]
colnames.append(desc[0] or fn)
coltypes.append(desc[1] or '')
coloptions.append(desc[2] or '')
colwidths.append(desc[3] or '100')

else:
# nothing found
# guess
colnames.append(fn)
coltypes.append(guess_type(f[1]))
coloptions.append('')
colwidths.append('100')

return colnames, coltypes, coloptions, colwidths

@frappe.whitelist()
def runquery(q='', ret=0, from_export=0):
import frappe.utils

formatted = cint(frappe.form_dict.get('formatted'))

# CASE A: Simple Query
# --------------------
if frappe.form_dict.get('simple_query') or frappe.form_dict.get('is_simple'):
if not q: q = frappe.form_dict.get('simple_query') or frappe.form_dict.get('query')
if q.split()[0].lower() != 'select':
raise Exception('Query must be a SELECT')

as_dict = cint(frappe.form_dict.get('as_dict'))
res = frappe.db.sql(q, as_dict = as_dict, as_list = not as_dict, formatted=formatted)

# build colnames etc from metadata
colnames, coltypes, coloptions, colwidths = [], [], [], []

# CASE B: Standard Query
# -----------------------
else:
if not q: q = frappe.form_dict.get('query')

tl = get_sql_tables(q)
meta = get_sql_meta(tl)

q = add_match_conditions(q, tl)

# replace special variables
q = q.replace('__user', frappe.session.user)
q = q.replace('__today', frappe.utils.nowdate())

res = frappe.db.sql(q, as_list=1, formatted=formatted)

colnames, coltypes, coloptions, colwidths = build_description_standard(meta, tl)

# run server script
# -----------------
style, header_html, footer_html, page_template = '', '', '', ''

out['colnames'] = colnames
out['coltypes'] = coltypes
out['coloptions'] = coloptions
out['colwidths'] = colwidths
out['header_html'] = header_html
out['footer_html'] = footer_html
out['page_template'] = page_template

if style:
out['style'] = style

# just the data - return
if ret==1:
return res

out['values'] = res

# return num of entries
qm = frappe.form_dict.get('query_max') or ''
if qm and qm.strip():
if qm.split()[0].lower() != 'select':
raise Exception('Query (Max) must be a SELECT')
if not frappe.form_dict.get('simple_query'):
qm = add_match_conditions(qm, tl)

out['n_values'] = frappe.utils.cint(frappe.db.sql(qm)[0][0])


@frappe.whitelist()
def runquery_csv():
global out

q = frappe.form_dict.get('query')

rep_name = frappe.form_dict.get('report_name')
if not frappe.form_dict.get('simple_query'):

# Report Name
if not rep_name:
rep_name = get_sql_tables(q)[0]

if not rep_name: rep_name = 'DataExport'

rows = [[rep_name], out['colnames']] + out['values']

from six import StringIO
import csv

f = StringIO()
writer = csv.writer(f)
for r in rows:
# encode only unicode type strings and not int, floats etc.
writer.writerow(map(lambda v: isinstance(v, text_type) and v.encode('utf-8') or v, r))

f.seek(0)
out['result'] = text_type(f.read(), 'utf-8')
out['type'] = 'csv'
out['doctype'] = rep_name

def add_limit_to_query(query, args):
"""
Add limit condition to query
can be used by methods called in listing to add limit condition
"""
if args.get('limit_page_length'):
query += """
limit %(limit_start)s, %(limit_page_length)s"""

import frappe.utils
args['limit_start'] = frappe.utils.cint(args.get('limit_start'))
args['limit_page_length'] = frappe.utils.cint(args.get('limit_page_length'))

return query, args

+ 3
- 0
frappe/desk/query_report.py Переглянути файл

@@ -462,6 +462,9 @@ def add_total_row(result, columns, meta = None):
@frappe.whitelist()
def get_data_for_custom_field(doctype, field):

if not frappe.has_permission(doctype, "read"):
frappe.throw(_("Not Permitted"), frappe.PermissionError)

value_map = frappe._dict(frappe.get_all(doctype,
fields=["name", field],
as_list=1))


+ 1
- 0
frappe/desk/reportview.py Переглянути файл

@@ -36,6 +36,7 @@ def get_form_params():
data.pop('data', None)
data.pop('ignore_permissions', None)
data.pop('view', None)
data.pop('user', None)

if "csrf_token" in data:
del data["csrf_token"]


+ 2
- 3
frappe/desk/treeview.py Переглянути файл

@@ -6,12 +6,11 @@ import frappe
from frappe import _

@frappe.whitelist()
def get_all_nodes(doctype, parent, tree_method, **filters):
def get_all_nodes(doctype, label, parent, tree_method, **filters):
'''Recursively gets all data from tree nodes'''

if 'cmd' in filters:
del filters['cmd']

filters.pop('data', None)

tree_method = frappe.get_attr(tree_method)
@@ -20,7 +19,7 @@ def get_all_nodes(doctype, parent, tree_method, **filters):
frappe.throw(_("Not Permitted"), frappe.PermissionError)

data = tree_method(doctype, parent, **filters)
out = [dict(parent=parent, data=data)]
out = [dict(parent=label, data=data)]

if 'is_root' in filters:
del filters['is_root']


+ 0
- 3
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);



+ 39
- 9
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",


+ 19
- 21
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 or 'Rich Text']

def get_recipients(self):
"""Get recipients from Email Group"""


+ 4
- 2
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"];
}


+ 1
- 0
frappe/email/doctype/newsletter/test_newsletter.py Переглянути файл

@@ -67,6 +67,7 @@ class TestNewsletter(unittest.TestCase):
"doctype": "Newsletter",
"subject": "_Test Newsletter",
"send_from": "Test Sender <test_sender@example.com>",
"content_type": "Rich Text",
"message": "Testing my news.",
"published": published,
"schedule_sending": bool(schedule_send),


+ 20
- 5
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) {


+ 10
- 1
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"
},
{
@@ -278,9 +279,17 @@
"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-09-03 10:33:23.084590",
"modified_by": "Administrator",


+ 28
- 7
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
@@ -196,6 +197,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))
@@ -241,13 +243,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)

@@ -269,6 +278,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))
@@ -411,4 +423,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

+ 88
- 1
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)



+ 2
- 1
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",


+ 1
- 1
frappe/email/email_body.py Переглянути файл

@@ -207,7 +207,7 @@ 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
self.msg_root["In-Reply-To"] = in_reply_to.replace("\r", "").replace("\n", "")

def make(self):
"""build into msg_root"""


+ 1
- 1
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)


+ 6
- 13
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": [
{
@@ -22,6 +21,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Callback URL",
"read_only": 1,
"reqd": 1,
"unique": 1
},
@@ -29,19 +29,20 @@
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
"read_only": 1
"reqd": 1
},
{
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
"read_only": 1
"reqd": 1
},
{
"fieldname": "user",
"fieldtype": "Link",
"label": "Event Subscriber",
"options": "User",
"read_only": 1,
"reqd": 1
},
{
@@ -60,14 +61,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 +71,7 @@
],
"in_create": 1,
"links": [],
"modified": "2019-12-30 11:52:16.276047",
"modified": "2020-09-08 16:42:39.828085",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Consumer",


+ 23
- 14
frappe/event_streaming/doctype/event_consumer/event_consumer.py Переглянути файл

@@ -6,6 +6,8 @@ from __future__ import unicode_literals
import frappe
import json
import requests
import os
from frappe import _
from frappe.model.document import Document
from frappe.frappeclient import FrappeClient
from frappe.utils.data import get_url
@@ -14,13 +16,18 @@ 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:
doc_before_save = self.get_doc_before_save()
if doc_before_save.api_key != self.api_key or doc_before_save.api_secret != self.api_secret:
return

self.update_consumer_status()
else:
frappe.db.set_value(self.doctype, self.name, 'incoming_change', 0)
@@ -56,17 +63,26 @@ class EventConsumer(Document):
return 'offline'
return 'online'


@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def register_consumer(data):
"""create an event consumer document for registering a consumer"""
data = json.loads(data)
# to ensure that consumer is created only once
if frappe.db.exists('Event Consumer', data['event_consumer']):
return None

user = data['user']
if not frappe.db.exists('User', user):
frappe.throw(_('User {0} not found on the producer site').format(user))

if "System Manager" not in frappe.get_roles(user):
frappe.throw(_("Event Subscriber has to be a System Manager."))

consumer = frappe.new_doc('Event Consumer')
consumer.callback_url = data['event_consumer']
consumer.user = data['user']
consumer.api_key = data['api_key']
consumer.api_secret = data['api_secret']
consumer.incoming_change = True
consumer_doctypes = json.loads(data['consumer_doctypes'])

@@ -76,19 +92,13 @@ def register_consumer(data):
'status': 'Pending'
})

api_key = frappe.generate_hash(length=10)
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()
consumer.insert()

# consumer's 'last_update' field should point to the latest update
# in producer's update log when subscribing
# so that, updates after subscribing are consumed and not the old ones.
last_update = str(get_last_update())
return json.dumps({'api_key': api_key, 'api_secret': api_secret, 'last_update': last_update})
return json.dumps({'last_update': last_update})


def get_consumer_site(consumer_url):
@@ -97,8 +107,7 @@ def get_consumer_site(consumer_url):
consumer_site = FrappeClient(
url=consumer_url,
api_key=consumer_doc.api_key,
api_secret=consumer_doc.get_password('api_secret'),
frappe_authorization_source='Event Producer'
api_secret=consumer_doc.get_password('api_secret')
)
return consumer_site



+ 7
- 4
frappe/event_streaming/doctype/event_producer/event_producer.json Переглянути файл

@@ -32,23 +32,26 @@
"read_only": 1
},
{
"description": "API Key of the user(Event Subscriber) on the producer site",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
"read_only": 1
"reqd": 1
},
{
"description": "API Secret of the user(Event Subscriber) on the producer site",
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
"read_only": 1
"reqd": 1
},
{
"fieldname": "user",
"fieldtype": "Link",
"label": "Event Subscriber",
"options": "User",
"reqd": 1
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "column_break_6",
@@ -74,7 +77,7 @@
}
],
"links": [],
"modified": "2019-12-26 13:04:11.438349",
"modified": "2020-09-08 18:50:57.687979",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Producer",


+ 31
- 11
frappe/event_streaming/doctype/event_producer/event_producer.py Переглянути файл

@@ -12,7 +12,8 @@ from frappe import _
from frappe.model.document import Document
from frappe.frappeclient import FrappeClient
from frappe.utils.background_jobs import get_jobs
from frappe.utils.data import get_url
from frappe.utils.data import get_url, get_link_to_form
from frappe.utils.password import get_decrypted_password
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.integrations.oauth2 import validate_url

@@ -20,19 +21,35 @@ from frappe.integrations.oauth2 import validate_url
class EventProducer(Document):
def before_insert(self):
self.check_url()
self.validate_event_subscriber()
self.incoming_change = True
self.create_event_consumer()
self.create_custom_fields()

def validate(self):
self.validate_event_subscriber()
if frappe.flags.in_test:
for entry in self.producer_doctypes:
entry.status = 'Approved'

def validate_event_subscriber(self):
if not frappe.db.get_value('User', self.user, 'api_key'):
frappe.throw(_('Please generate keys for the Event Subscriber User {0} first.').format(
frappe.bold(get_link_to_form('User', self.user))
))

def on_update(self):
if not self.incoming_change:
self.update_event_consumer()
self.create_custom_fields()
if frappe.db.exists('Event Producer', self.name):
if not self.api_key or not self.api_secret:
frappe.throw(_('Please set API Key and Secret on the producer and consumer sites first.'))
else:
doc_before_save = self.get_doc_before_save()
if doc_before_save.api_key != self.api_key or doc_before_save.api_secret != self.api_secret:
return

self.update_event_consumer()
self.create_custom_fields()
else:
# when producer doc is updated it updates the consumer doc, set flag to avoid deadlock
self.db_set('incoming_change', 0)
@@ -50,15 +67,18 @@ class EventProducer(Document):
def create_event_consumer(self):
"""register event consumer on the producer site"""
if self.is_producer_online():
producer_site = FrappeClient(self.producer_url, verify=False)
producer_site = FrappeClient(
url=self.producer_url,
api_key=self.api_key,
api_secret=self.get_password('api_secret')
)

response = producer_site.post_api(
'frappe.event_streaming.doctype.event_consumer.event_consumer.register_consumer',
params={'data': json.dumps(self.get_request_data())}
)
if response:
response = json.loads(response)
self.api_key = response['api_key']
self.api_secret = response['api_secret']
self.last_update = response['last_update']
else:
frappe.throw(_('Failed to create an Event Consumer or an Event Consumer for the current site is already registered.'))
@@ -72,11 +92,14 @@ class EventProducer(Document):
else:
consumer_doctypes.append(entry.ref_doctype)

user_key = frappe.db.get_value('User', self.user, 'api_key')
user_secret = get_decrypted_password('User', self.user, 'api_secret')
return {
'event_consumer': get_url(),
'consumer_doctypes': json.dumps(consumer_doctypes),
'user': self.user,
'in_test': frappe.flags.in_test
'api_key': user_key,
'api_secret': user_secret
}

def create_custom_fields(self):
@@ -110,8 +133,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)
@@ -134,8 +155,7 @@ def get_producer_site(producer_url):
producer_site = FrappeClient(
url=producer_url,
api_key=producer_doc.api_key,
api_secret=producer_doc.get_password('api_secret'),
frappe_authorization_source='Event Consumer'
api_secret=producer_doc.get_password('api_secret')
)
return producer_site



+ 34
- 14
frappe/event_streaming/doctype/event_producer/test_event_producer.py Переглянути файл

@@ -8,6 +8,7 @@ import unittest
import json
from frappe.frappeclient import FrappeClient
from frappe.event_streaming.doctype.event_producer.event_producer import pull_from_node
from frappe.core.doctype.user.user import generate_keys

producer_url = 'http://test_site_producer:8000'

@@ -166,16 +167,6 @@ class TestEventProducer(unittest.TestCase):
def pull_producer_data(self):
pull_from_node(producer_url)

def get_remote_site(self):
producer_doc = frappe.get_doc('Event Producer', producer_url)
producer_site = FrappeClient(
url=producer_doc.producer_url,
api_key=producer_doc.api_key,
api_secret=producer_doc.get_password('api_secret'),
frappe_authorization_source='Event Consumer'
)
return producer_site

def test_mapping(self):
producer = get_remote_site()
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
@@ -298,6 +289,20 @@ def create_event_producer(producer_url):
event_producer.save()
return

generate_keys('Administrator')

producer_site = connect()

response = producer_site.post_api(
'frappe.core.doctype.user.user.generate_keys',
params={'user': 'Administrator'}
)

api_secret = response.get('api_secret')

response = producer_site.get_value('User', 'api_key', {'name': 'Administrator'})
api_key = response.get('api_key')

event_producer = frappe.new_doc('Event Producer')
event_producer.producer_doctypes = []
event_producer.producer_url = producer_url
@@ -310,6 +315,8 @@ def create_event_producer(producer_url):
'use_same_name': 1
})
event_producer.user = 'Administrator'
event_producer.api_key = api_key
event_producer.api_secret = api_secret
event_producer.save()

def reset_configuration(producer_url):
@@ -331,9 +338,9 @@ def get_remote_site():
producer_doc = frappe.get_doc('Event Producer', producer_url)
producer_site = FrappeClient(
url=producer_doc.producer_url,
api_key=producer_doc.api_key,
api_secret=producer_doc.get_password('api_secret'),
frappe_authorization_source='Event Consumer'
username='Administrator',
password='admin',
verify=False
)
return producer_site

@@ -341,4 +348,17 @@ def unsubscribe_doctypes(producer_url):
event_producer = frappe.get_doc('Event Producer', producer_url)
for entry in event_producer.producer_doctypes:
entry.unsubscribe = 1
event_producer.save()
event_producer.save()

def connect():
def _connect():
return FrappeClient(
url=producer_url,
username='Administrator',
password='admin',
verify=False
)
try:
return _connect()
except Exception:
return _connect()

+ 4
- 1
frappe/installer.py Переглянути файл

@@ -359,8 +359,11 @@ def is_downgrade(sql_file_path, verbose=False):
if head in line:
# 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master')
line = line.strip().lstrip(head).rstrip(";").strip()
app_rows = frappe.safe_eval(line)
# check if iterable consists of tuples before trying to transform
apps_list = app_rows if all(isinstance(app_row, (tuple, list, set)) for app_row in app_rows) else (app_rows, )
# 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')]
all_apps = [ x[-3:] for x in frappe.safe_eval(line) ]
all_apps = [ x[-3:] for x in apps_list ]

for app in all_apps:
app_name = app[0]


+ 6
- 0
frappe/integrations/doctype/razorpay_settings/razorpay_settings.py Переглянути файл

@@ -65,6 +65,7 @@ import frappe
from frappe import _
import json
import hmac
import razorpay
import hashlib
from six.moves.urllib.parse import urlencode
from frappe.model.document import Document
@@ -75,6 +76,11 @@ from frappe.integrations.utils import (make_get_request, make_post_request, crea
class RazorpaySettings(Document):
supported_currencies = ["INR"]

def init_client(self):
if self.api_key:
secret = self.get_password(fieldname="api_secret", raise_exception=False)
self.client = razorpay.Client(auth=(self.api_key, secret))

def validate(self):
create_payment_gateway('Razorpay')
call_hook_method('payment_gateway_enabled', gateway='Razorpay')


+ 2
- 2
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"))


+ 2
- 1
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):


+ 1
- 0
frappe/model/db_query.py Переглянути файл

@@ -769,6 +769,7 @@ def get_list(doctype, *args, **kwargs):
kwargs.pop('ignore_permissions', None)
kwargs.pop('data', None)
kwargs.pop('strict', None)
kwargs.pop('user', None)

# If doctype is child table
if frappe.is_table(doctype):


+ 2
- 2
frappe/model/document.py Переглянути файл

@@ -905,9 +905,9 @@ class Document(BaseDocument):
"""Cancel the document. Sets `docstatus` = 2, then saves."""
self._cancel()

def delete(self):
def delete(self, ignore_permissions=False):
"""Delete document."""
frappe.delete_doc(self.doctype, self.name, flags=self.flags)
frappe.delete_doc(self.doctype, self.name, ignore_permissions = ignore_permissions, flags=self.flags)

def run_before_save_methods(self):
"""Run standard methods before `INSERT` or `UPDATE`. Standard Methods are:


+ 2
- 0
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:


+ 0
- 1
frappe/model/rename_doc.py Переглянути файл

@@ -25,7 +25,6 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne

return docname

@frappe.whitelist()
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False, show_alert=True):
"""
Renames a doc(dt, old) to doc(dt, new) and


+ 4
- 0
frappe/patches.txt Переглянути файл

@@ -301,6 +301,10 @@ 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
frappe.patches.v13_0.delete_event_producer_and_consumer_keys

+ 44
- 0
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 '%&lt;%' or bcc like '%&lt;%' or recipients like '%&lt;%')
''', 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', '%&lt;%']},
['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 '%&lt;%'
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 ('&lt;' in email and '&gt;' in email):
return email.replace('&gt;', '>').replace('&lt;', '<')

return email

+ 11
- 0
frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py Переглянути файл

@@ -0,0 +1,11 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe

def execute():
if frappe.db.exists("DocType", "Event Producer"):
frappe.db.sql("""UPDATE `tabEvent Producer` SET api_key='', api_secret=''""")
if frappe.db.exists("DocType", "Event Consumer"):
frappe.db.sql("""UPDATE `tabEvent Consumer` SET api_key='', api_secret=''""")

+ 13
- 0
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
""")

+ 12
- 0
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'
""")

+ 6
- 3
frappe/printing/doctype/print_format/print_format.json Переглянути файл

@@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-01-23 19:54:43",
@@ -181,7 +182,7 @@
"fieldname": "print_format_help",
"fieldtype": "HTML",
"label": "Print Format Help",
"options": "<h3>Print Format Help</h3>\n<hr>\n<h4>Introduction</h4>\n<p>Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the <code>doc</code> object which contains information about the document that is being formatted. You can also access common utilities via the <code>frappe</code> module.</p>\n<p>For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.</p>\n<hr>\n<h4>References</h4>\n<ol>\n\t<li><a href=\"http://jinja.pocoo.org/docs/templates/\" target=\"_blank\">Jinja Tempalting Language: Reference</a></li>\n\t<li><a href=\"http://getbootstrap.com\" target=\"_blank\">Bootstrap CSS Framework</a></li>\n</ol>\n<hr>\n<h4>Example</h4>\n<pre><code>&lt;h3&gt;{{ doc.select_print_heading or \"Invoice\" }}&lt;/h3&gt;\n&lt;div class=\"row\"&gt;\n\t&lt;div class=\"col-md-3 text-right\"&gt;Customer Name&lt;/div&gt;\n\t&lt;div class=\"col-md-9\"&gt;{{ doc.customer_name }}&lt;/div&gt;\n&lt;/div&gt;\n&lt;div class=\"row\"&gt;\n\t&lt;div class=\"col-md-3 text-right\"&gt;Date&lt;/div&gt;\n\t&lt;div class=\"col-md-9\"&gt;{{ doc.get_formatted(\"invoice_date\") }}&lt;/div&gt;\n&lt;/div&gt;\n&lt;table class=\"table table-bordered\"&gt;\n\t&lt;tbody&gt;\n\t\t&lt;tr&gt;\n\t\t\t&lt;th&gt;Sr&lt;/th&gt;\n\t\t\t&lt;th&gt;Item Name&lt;/th&gt;\n\t\t\t&lt;th&gt;Description&lt;/th&gt;\n\t\t\t&lt;th class=\"text-right\"&gt;Qty&lt;/th&gt;\n\t\t\t&lt;th class=\"text-right\"&gt;Rate&lt;/th&gt;\n\t\t\t&lt;th class=\"text-right\"&gt;Amount&lt;/th&gt;\n\t\t&lt;/tr&gt;\n\t\t{%- for row in doc.items -%}\n\t\t&lt;tr&gt;\n\t\t\t&lt;td style=\"width: 3%;\"&gt;{{ row.idx }}&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 20%;\"&gt;\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t&lt;br&gt;Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 37%;\"&gt;\n\t\t\t\t&lt;div style=\"border: 0px;\"&gt;{{ row.description }}&lt;/div&gt;&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 10%; text-align: right;\"&gt;{{ row.qty }} {{ row.uom or row.stock_uom }}&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 15%; text-align: right;\"&gt;{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 15%; text-align: right;\"&gt;{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}&lt;/td&gt;\n\t\t&lt;/tr&gt;\n\t\t{%- endfor -%}\n\t&lt;/tbody&gt;\n&lt;/table&gt;</code></pre>\n<hr>\n<h4>Common Functions</h4>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td style=\"width: 30%;\"><code>doc.get_formatted(\"[fieldname]\", [parent_doc])</code></td>\n\t\t\t<td>Get document value formatted as Date, Currency etc. Pass parent <code>doc</code> for curreny type fields.</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td style=\"width: 30%;\"><code>frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")</code></td>\n\t\t\t<td>Get value from another document.</td>\n\t\t</tr>\n\t</tbody>\n</table>\n"
"options": "<h3>Print Format Help</h3>\n<hr>\n<h4>Introduction</h4>\n<p>Print Formats are rendered on the server side using the Jinja Templating Language. All forms have access to the <code>doc</code> object which contains information about the document that is being formatted. You can also access common utilities via the <code>frappe</code> module.</p>\n<p>For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.</p>\n<hr>\n<h4>References</h4>\n<ol>\n\t<li><a href=\"http://jinja.pocoo.org/docs/templates/\" target=\"_blank\">Jinja Templating Language</a></li>\n\t<li><a href=\"http://getbootstrap.com\" target=\"_blank\">Bootstrap CSS Framework</a></li>\n</ol>\n<hr>\n<h4>Example</h4>\n<pre><code>&lt;h3&gt;{{ doc.select_print_heading or \"Invoice\" }}&lt;/h3&gt;\n&lt;div class=\"row\"&gt;\n\t&lt;div class=\"col-md-3 text-right\"&gt;Customer Name&lt;/div&gt;\n\t&lt;div class=\"col-md-9\"&gt;{{ doc.customer_name }}&lt;/div&gt;\n&lt;/div&gt;\n&lt;div class=\"row\"&gt;\n\t&lt;div class=\"col-md-3 text-right\"&gt;Date&lt;/div&gt;\n\t&lt;div class=\"col-md-9\"&gt;{{ doc.get_formatted(\"invoice_date\") }}&lt;/div&gt;\n&lt;/div&gt;\n&lt;table class=\"table table-bordered\"&gt;\n\t&lt;tbody&gt;\n\t\t&lt;tr&gt;\n\t\t\t&lt;th&gt;Sr&lt;/th&gt;\n\t\t\t&lt;th&gt;Item Name&lt;/th&gt;\n\t\t\t&lt;th&gt;Description&lt;/th&gt;\n\t\t\t&lt;th class=\"text-right\"&gt;Qty&lt;/th&gt;\n\t\t\t&lt;th class=\"text-right\"&gt;Rate&lt;/th&gt;\n\t\t\t&lt;th class=\"text-right\"&gt;Amount&lt;/th&gt;\n\t\t&lt;/tr&gt;\n\t\t{%- for row in doc.items -%}\n\t\t&lt;tr&gt;\n\t\t\t&lt;td style=\"width: 3%;\"&gt;{{ row.idx }}&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 20%;\"&gt;\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t&lt;br&gt;Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 37%;\"&gt;\n\t\t\t\t&lt;div style=\"border: 0px;\"&gt;{{ row.description }}&lt;/div&gt;&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 10%; text-align: right;\"&gt;{{ row.qty }} {{ row.uom or row.stock_uom }}&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 15%; text-align: right;\"&gt;{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}&lt;/td&gt;\n\t\t\t&lt;td style=\"width: 15%; text-align: right;\"&gt;{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}&lt;/td&gt;\n\t\t&lt;/tr&gt;\n\t\t{%- endfor -%}\n\t&lt;/tbody&gt;\n&lt;/table&gt;</code></pre>\n<hr>\n<h4>Common Functions</h4>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td style=\"width: 30%;\"><code>doc.get_formatted(\"[fieldname]\", [parent_doc])</code></td>\n\t\t\t<td>Get document value formatted as Date, Currency, etc. Pass parent <code>doc</code> for currency type fields.</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td style=\"width: 30%;\"><code>frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")</code></td>\n\t\t\t<td>Get value from another document.</td>\n\t\t</tr>\n\t</tbody>\n</table>\n"
},
{
"fieldname": "format_data",
@@ -199,8 +200,10 @@
],
"icon": "fa fa-print",
"idx": 1,
"modified": "2019-11-28 12:40:40.364699",
"modified_by": "faris@erpnext.com",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-29 11:44:59.082797",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
"owner": "Administrator",


+ 1
- 1
frappe/public/build.json Переглянути файл

@@ -128,7 +128,7 @@
"public/js/lib/Sortable.min.js",
"public/js/lib/jquery/jquery.hotkeys.js",
"public/js/lib/bootstrap.min.js",
"node_modules/vue/dist/vue.js",
"node_modules/vue/dist/vue.min.js",
"node_modules/moment/min/moment-with-locales.min.js",
"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
"public/js/lib/socket.io.min.js",


+ 1
- 1
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]));
}
},


+ 44
- 17
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<l; i++) {
var p = docperms[i];
perm[p.permlevel || 0] = {
const docperms = frappe.perm.get_perm(this.doc.doctype);
this.perm = docperms.map(p => {
return {
read: p.read,
cancel: p.cancel,
share: p.share,
print: p.print,
email: p.email
};
}
this.perm = perm;
});
}

trigger(event, doctype, docname) {


+ 19
- 24
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
}

$.each(frappe.perm.rights, (i, key) => {
perm[permlevel][key] = perm[permlevel][key] || (p[key] || 0);
});
let permlevel = cint(p.permlevel);
if (!perm[permlevel]) {
perm[permlevel] = {};
perm[permlevel]["permlevel"] = permlevel;
}
});

// remove values with 0
$.each(perm[0], (key, val) => {
if (!val) {
delete perm[0][key];
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;
}
});
}
});

$.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) => {


+ 17
- 13
frappe/public/js/frappe/ui/tree.js Переглянути файл

@@ -5,17 +5,20 @@ frappe.provide('frappe.ui');

frappe.ui.Tree = class {
constructor({
parent, label, icon_set, toolbar, expandable, with_skeleton=1, // eslint-disable-line
parent, label, root_value, icon_set, toolbar, expandable, with_skeleton=1, // eslint-disable-line

args, method, get_label, on_render, on_click // eslint-disable-line
}) {
$.extend(this, arguments[0]);
if (root_value == null) {
this.root_value = label;
}
this.setup_treenode_class();
this.nodes = {};
this.wrapper = $('<div class="tree">').appendTo(this.parent);
if(with_skeleton) this.wrapper.addClass('with-skeleton');
if (with_skeleton) this.wrapper.addClass('with-skeleton');

if(!icon_set) {
if (!icon_set) {
this.icon_set = {
open: 'fa fa-fw fa-folder-open',
closed: 'fa fa-fw fa-folder',
@@ -42,8 +45,9 @@ frappe.ui.Tree = class {
});
}

get_all_nodes(value, is_root) {
get_all_nodes(value, is_root, label) {
var args = Object.assign({}, this.args);
args.label = label || value;
args.parent = value;
args.is_root = is_root;

@@ -88,7 +92,7 @@ frappe.ui.Tree = class {
expandable: true,
is_root: true,
data: {
value: this.label
value: this.root_value
}
});
this.expand_node(this.root_node, false);
@@ -144,25 +148,25 @@ frappe.ui.Tree = class {
}

load_children(node, deep=false) {
let value = node.data.value, is_root = node.is_root;
let lab = node.label, value = node.data.value, is_root = node.is_root;

if(!deep) {
frappe.run_serially([
() => {return this.get_nodes(value, is_root);},
(data_set) => { this.render_node_children(node, data_set); },
() => { this.set_selected_node(node); }
() => this.get_nodes(value, is_root),
(data_set) => this.render_node_children(node, data_set),
() => this.set_selected_node(node)
]);
} else {
frappe.run_serially([
() => {return this.get_all_nodes(value, is_root);},
(data_list) => { this.render_children_of_all_nodes(data_list); },
() => { this.set_selected_node(node); }
() => this.get_all_nodes(value, is_root, lab),
(data_list) => this.render_children_of_all_nodes(data_list),
() => this.set_selected_node(node)
]);
}
}

render_children_of_all_nodes(data_list) {
data_list.map(d => { this.render_node_children(this.nodes[d.parent], d.data); });
data_list.map(d => this.render_node_children(this.nodes[d.parent], d.data));
}

render_node_children(node, data_set) {


+ 3
- 1
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);
@@ -1041,7 +1043,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {

if (column.colIndex === index && !value) {
value = "Total";
column.fieldtype = "Data"; // avoid type issues for value if Date column
column = { fieldtype: "Data" }; // avoid type issues for value if Date column
} else if (in_list(["Currency", "Float"], column.fieldtype)) {
// proxy for currency and float
data = this.data[0];


+ 15
- 2
frappe/public/js/frappe/views/treeview.js Переглянути файл

@@ -39,6 +39,7 @@ frappe.views.TreeView = Class.extend({
this.get_permissions();
this.make_page();
this.make_filters();
this.root_value = null;

if (me.opts.get_tree_root) {
this.get_root();
@@ -129,7 +130,13 @@ frappe.views.TreeView = Class.extend({
args: me.args,
callback: function(r) {
if (r.message) {
me.root_label = r.message[0]["value"];
if (r.message.length > 1) {
me.root_label = me.doctype;
me.root_value = "";
} else {
me.root_label = r.message[0]["value"];
me.root_value = me.root_label;
}
me.make_tree();
}
}
@@ -138,9 +145,15 @@ frappe.views.TreeView = Class.extend({
make_tree: function() {
$(this.parent).find(".tree").remove();

var use_label = this.args[this.opts.root_label] || this.root_label || this.opts.root_label;
var use_value = this.root_value;
if (use_value == null) {
use_value = use_label;
}
this.tree = new frappe.ui.Tree({
parent: this.body,
label: this.args[this.opts.root_label] || this.root_label || this.opts.root_label,
label: use_label,
root_value: use_value,
expandable: true,

args: this.args,


+ 10
- 1
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)


+ 3
- 0
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):


+ 1
- 1
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?,آیا می خواهید کلیه اسناد مرتبط را لغو کنید؟,


+ 2
- 1
frappe/utils/__init__.py Переглянути файл

@@ -135,7 +135,8 @@ def validate_email_address(email_str, throw=False):

if not _valid:
if throw:
frappe.throw(frappe._("{0} is not a valid Email Address").format(e),
invalid_email = frappe.utils.escape_html(e)
frappe.throw(frappe._("{0} is not a valid Email Address").format(invalid_email),
frappe.InvalidEmailAddressError)
return None
else:


+ 24
- 15
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:
@@ -51,10 +52,23 @@ class BackupGenerator:

site = frappe.local.site or frappe.generate_hash(length=8)
self.site_slug = site.replace('.', '_')

self.verbose = verbose
self.setup_backup_directory()
_verbose = verbose

def setup_backup_directory(self):
specified = self.backup_path_db or self.backup_path_files or self.backup_path_private_files

if not specified:
backups_folder = get_backup_path()
if not os.path.exists(backups_folder):
os.makedirs(backups_folder)
else:
for file_path in [self.backup_path_files, self.backup_path_db, self.backup_path_private_files]:
dir = os.path.dirname(file_path)
os.makedirs(dir, exist_ok=True)


def get_backup(self, older_than=24, ignore_files=False, force=False):
"""
Takes a new dump if existing file is old
@@ -86,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:
@@ -153,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


+ 1
- 23
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)


+ 4
- 0
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:


+ 15
- 2
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')

+ 119
- 3
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"
)

+ 14
- 1
frappe/website/doctype/blog_post/blog_post.js Переглянути файл

@@ -11,18 +11,31 @@ 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.meta_title || frm.doc.title)) return;
let google_preview = frm.get_field("google_preview");
let seo_title = (frm.doc.title).slice(0, 60);
let seo_title = (frm.doc.meta_title || frm.doc.title).slice(0, 60);
let seo_description = (frm.doc.meta_description || frm.doc.blog_intro || "").slice(0, 160);
let date = frm.doc.published_on ? new frappe.datetime.datetime(frm.doc.published_on).moment.format('ll') + ' - ' : '';
let route_array = frm.doc.route ? frm.doc.route.split('/') : [];


+ 8
- 1
frappe/website/doctype/blog_post/blog_post.json Переглянути файл

@@ -26,6 +26,7 @@
"content_html",
"email_sent",
"meta_tags",
"meta_title",
"meta_description",
"column_break_18",
"meta_image",
@@ -184,6 +185,12 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Hide CTA"
},
{
"fieldname": "meta_title",
"fieldtype": "Data",
"label": "Meta Title",
"length": 60
}
],
"has_web_view": 1,
@@ -193,7 +200,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 5,
"modified": "2020-07-21 16:25:17.154911",
"modified": "2020-08-31 21:01:51.100349",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",


+ 6
- 1
frappe/website/doctype/blog_post/blog_post.py Переглянути файл

@@ -36,6 +36,11 @@ class BlogPost(WebsiteGenerator):
if self.blog_intro:
self.blog_intro = self.blog_intro[:200]

if not self.meta_title:
self.meta_title = self.title[:60]
else:
self.meta_title = self.meta_title[:60]

if not self.meta_description:
self.meta_description = self.blog_intro[:140]
else:
@@ -88,7 +93,7 @@ class BlogPost(WebsiteGenerator):
context.description = self.meta_description or self.blog_intro or strip_html_tags(context.content[:140])

context.metatags = {
"name": self.title,
"name": self.meta_title,
"description": context.description,
}



+ 2
- 1
frappe/website/doctype/web_page/web_page.json Переглянути файл

@@ -126,6 +126,7 @@
"depends_on": "eval:doc.content_type==='Markdown'",
"fieldname": "main_section_md",
"fieldtype": "Markdown Editor",
"ignore_xss_filter": 1,
"label": "Main Section (Markdown)"
},
{
@@ -294,7 +295,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 20,
"modified": "2020-08-07 10:55:54.885448",
"modified": "2020-08-31 16:55:52.015249",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page",


+ 3
- 0
frappe/website/web_template/section_with_tabs/section_with_tabs.html Переглянути файл

@@ -1,5 +1,8 @@
<h2 class="section-title">{{ title }}</h2>

{%- if subtitle -%}
<p class="section-description">{{ subtitle }}</p>
{%- endif -%}

<div class="mt-5">
{% set ns = namespace(tabs=[]) %}


+ 1
- 0
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",


+ 2
- 1
requirements.txt Переглянути файл

@@ -71,4 +71,5 @@ zxcvbn-python==4.4.24
pycryptodome==3.9.8
paytmchecksum==1.7.0
wrapt==1.10.11
twilio==6.44.2
twilio==6.44.2
razorpay==1.2.0

+ 1
- 0
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,


+ 83
- 35
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"


Завантаження…
Відмінити
Зберегти