diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 52994ccec3..d92c727dc7 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -256,6 +256,15 @@ def migrate(context, rebuild_website=False, skip_failing=False): print("Compiling Python Files...") compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*')) +@click.command('migrate-to') +@click.argument('frappe_provider') +@pass_context +def migrate_to(context, frappe_provider): + "Migrates site to the specified provider" + from frappe.integrations.frappe_providers import migrate_to + for site in context.sites: + migrate_to(site, frappe_provider) + @click.command('run-patch') @click.argument('module') @pass_context @@ -322,18 +331,19 @@ def use(site, sites_path='.'): @click.command('backup') @click.option('--with-files', default=False, is_flag=True, help="Take backup with files") +@click.option('--verbose', default=False, is_flag=True) @pass_context def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, quiet=False): + backup_path_private_files=None, quiet=False, verbose=False): "Backup" from frappe.utils.backups import scheduled_backup - verbose = context.verbose + verbose = verbose or context.verbose exit_code = 0 for site in context.sites: try: frappe.init(site=site) frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose) except Exception as e: if verbose: print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site)) @@ -342,10 +352,12 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non if verbose: from frappe.utils import now - print("database backup taken -", odb.backup_path_db, "- on", now()) + summary_title = "Backup Summary at {0}".format(now()) + print(summary_title + "\n" + "-" * len(summary_title)) + print("Database backup:", odb.backup_path_db) if with_files: - print("files backup taken -", odb.backup_path_files, "- on", now()) - print("private files backup taken -", odb.backup_path_private_files, "- on", now()) + print("Public files: ", odb.backup_path_files) + print("Private files: ", odb.backup_path_private_files) frappe.destroy() sys.exit(exit_code) @@ -559,6 +571,7 @@ commands = [ install_app, list_apps, migrate, + migrate_to, new_site, reinstall, reload_doc, diff --git a/frappe/config/customization.py b/frappe/config/customization.py index 06eaa2ea00..3d587e6839 100644 --- a/frappe/config/customization.py +++ b/frappe/config/customization.py @@ -3,7 +3,7 @@ from frappe import _ def get_data(): return [ - { + { "label": _("Form Customization"), "icon": "fa fa-glass", "items": [ @@ -57,9 +57,9 @@ def get_data(): }, { "type": "doctype", - "label": _("Custom Tags"), - "name": "Tag Category", - "description": _("Add your own Tag Categories") + "label": _("Package"), + "name": "Package", + "description": _("Import and Export Packages.") } ] } diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 8ae2b740b5..83d3c18453 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -45,6 +45,7 @@ "report_hide", "remember_last_selected_value", "ignore_xss_filter", + "hide_border", "property_depends_on_section", "mandatory_depends_on", "column_break_38", @@ -464,12 +465,19 @@ "fieldname": "show_seconds", "fieldtype": "Check", "label": "Show Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-06 09:06:25.224411", + "modified": "2020-05-15 09:06:25.224411", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/installed_application/__init__.py b/frappe/core/doctype/installed_application/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/installed_application/installed_application.json b/frappe/core/doctype/installed_application/installed_application.json new file mode 100644 index 0000000000..1f32c557ce --- /dev/null +++ b/frappe/core/doctype/installed_application/installed_application.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2020-05-11 17:44:54.674657", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "app_name", + "app_version", + "git_branch" + ], + "fields": [ + { + "fieldname": "git_branch", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Git Branch", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "app_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Application Name", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "app_version", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Application Version", + "read_only": 1, + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-05-12 10:09:49.148087", + "modified_by": "Administrator", + "module": "Core", + "name": "Installed Application", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/installed_application/installed_application.py b/frappe/core/doctype/installed_application/installed_application.py new file mode 100644 index 0000000000..6bb12afc49 --- /dev/null +++ b/frappe/core/doctype/installed_application/installed_application.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 InstalledApplication(Document): + pass diff --git a/frappe/core/doctype/installed_applications/__init__.py b/frappe/core/doctype/installed_applications/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/installed_applications/installed_applications.js b/frappe/core/doctype/installed_applications/installed_applications.js new file mode 100644 index 0000000000..9a1fd5ac18 --- /dev/null +++ b/frappe/core/doctype/installed_applications/installed_applications.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Installed Applications', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/installed_applications/installed_applications.json b/frappe/core/doctype/installed_applications/installed_applications.json new file mode 100644 index 0000000000..f2345e66b2 --- /dev/null +++ b/frappe/core/doctype/installed_applications/installed_applications.json @@ -0,0 +1,42 @@ +{ + "actions": [], + "creation": "2020-05-11 17:45:41.587750", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "installed_applications" + ], + "fields": [ + { + "fieldname": "installed_applications", + "fieldtype": "Table", + "label": "Installed Applications", + "options": "Installed Application", + "read_only": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-05-12 10:09:14.310622", + "modified_by": "Administrator", + "module": "Core", + "name": "Installed Applications", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "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 +} \ No newline at end of file diff --git a/frappe/core/doctype/installed_applications/installed_applications.py b/frappe/core/doctype/installed_applications/installed_applications.py new file mode 100644 index 0000000000..aa0401f368 --- /dev/null +++ b/frappe/core/doctype/installed_applications/installed_applications.py @@ -0,0 +1,18 @@ +# -*- 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 InstalledApplications(Document): + def update_versions(self): + self.delete_key("installed_applications") + for app in frappe.utils.get_installed_apps_info(): + self.append("installed_applications", { + "app_name": app.get("app_name"), + "app_version": app.get("version"), + "git_branch": app.get("branch") + }) + self.save() \ No newline at end of file diff --git a/frappe/core/doctype/installed_applications/test_installed_applications.py b/frappe/core/doctype/installed_applications/test_installed_applications.py new file mode 100644 index 0000000000..ab9b849fa1 --- /dev/null +++ b/frappe/core/doctype/installed_applications/test_installed_applications.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 TestInstalledApplications(unittest.TestCase): + pass diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index 222a31a863..0d1337351e 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -6,7 +6,7 @@ frappe.provide('frappe.dashboards.chart_sources'); frappe.pages['dashboard'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ + frappe.ui.make_app_page({ parent: wrapper, title: __("Dashboard"), single_column: true @@ -21,7 +21,7 @@ frappe.pages['dashboard'].on_page_load = function(wrapper) { class Dashboard { constructor(wrapper) { this.wrapper = $(wrapper); - $(`
+ $(`
`).appendTo(this.wrapper.find(".page-content").empty()); this.container = this.wrapper.find(".dashboard-graph"); diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index d220e448df..77490c8c43 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -50,6 +50,7 @@ "allow_in_quick_entry", "ignore_xss_filter", "translatable", + "hide_border", "description", "permlevel", "width", @@ -57,379 +58,386 @@ ], "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", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_seconds", - "fieldtype": "Check", - "label": "Show Seconds", - "show_days": 1, - "show_seconds": 1 + "default": "1", + "depends_on": "eval:doc.fieldtype === \"Duration\";", + "fieldname": "show_seconds", + "fieldtype": "Check", + "label": "Show Seconds", + "show_days": 1, + "show_seconds": 1 }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_days", - "fieldtype": "Check", - "label": "Show Days", - "show_days": 1, - "show_seconds": 1 + "default": "1", + "depends_on": "eval:doc.fieldtype === \"Duration\";", + "fieldname": "show_days", + "fieldtype": "Check", + "label": "Show Days", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" } ], "icon": "fa fa-glass", "idx": 1, "links": [], - "modified": "2020-05-14 23:43:00.123572", + "modified": "2020-05-15 23:43:00.123572", "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": "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": "System Manager", + "share": 1, + "write": 1 } ], "search_fields": "dt,label,fieldtype,options", diff --git a/frappe/custom/doctype/custom_link/__init__.py b/frappe/custom/doctype/custom_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js new file mode 100644 index 0000000000..8662724b1a --- /dev/null +++ b/frappe/custom/doctype/custom_link/custom_link.js @@ -0,0 +1,20 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Custom Link', { + refresh: function(frm) { + frm.set_query("document_type", function () { + return { + filters: { + custom: 0, + istable: 0, + module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] + } + }; + }); + + frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() { + frappe.set_route('List', frm.doc.document_type); + }); + } +}); diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/custom/doctype/custom_link/custom_link.json new file mode 100644 index 0000000000..350e6b1c2d --- /dev/null +++ b/frappe/custom/doctype/custom_link/custom_link.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "autoname": "field:document_type", + "creation": "2020-04-08 15:16:44.342509", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "links" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" + } + ], + "links": [], + "modified": "2020-04-08 16:42:59.402671", + "modified_by": "Administrator", + "module": "Custom", + "name": "Custom Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/custom_link/custom_link.py b/frappe/custom/doctype/custom_link/custom_link.py new file mode 100644 index 0000000000..11316d5751 --- /dev/null +++ b/frappe/custom/doctype/custom_link/custom_link.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 CustomLink(Document): + pass diff --git a/frappe/custom/doctype/custom_link/test_custom_link.py b/frappe/custom/doctype/custom_link/test_custom_link.py new file mode 100644 index 0000000000..a292f73ad0 --- /dev/null +++ b/frappe/custom/doctype/custom_link/test_custom_link.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 TestCustomLink(unittest.TestCase): + pass diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index ebf01d11b3..6a54d9c7e6 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -76,7 +76,8 @@ docfield_properties = { 'remember_last_selected_value': 'Check', 'allow_bulk_edit': 'Check', 'auto_repeat': 'Link', - 'allow_in_quick_entry': 'Check' + 'allow_in_quick_entry': 'Check', + 'hide_border': 'Check' } allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 5876666485..f422c36e61 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -41,6 +41,7 @@ "allow_on_submit", "report_hide", "remember_last_selected_value", + "hide_border", "property_depends_on_section", "mandatory_depends_on", "column_break_33", @@ -59,361 +60,368 @@ ], "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>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>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", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_seconds", - "fieldtype": "Check", - "label": "Show Seconds", - "show_days": 1, - "show_seconds": 1 + "default": "1", + "depends_on": "eval:doc.fieldtype === \"Duration\";", + "fieldname": "show_seconds", + "fieldtype": "Check", + "label": "Show Seconds", + "show_days": 1, + "show_seconds": 1 }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_days", - "fieldtype": "Check", - "label": "Show Days", - "show_days": 1, - "show_seconds": 1 + "default": "1", + "depends_on": "eval:doc.fieldtype === \"Duration\";", + "fieldname": "show_days", + "fieldtype": "Check", + "label": "Show Days", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-14 23:45:46.810869", + "modified": "2020-05-15 23:45:46.810869", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/package_document_type/__init__.py b/frappe/custom/doctype/package_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json new file mode 100644 index 0000000000..6d011bd4e4 --- /dev/null +++ b/frappe/custom/doctype/package_document_type/package_document_type.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "creation": "2020-05-14 16:45:47.196395", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_2", + "attachments", + "overwrite", + "section_break_4", + "filters_json" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "attachments", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Include Attachments" + }, + { + "default": "0", + "fieldname": "overwrite", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Overwrite" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "filters_json", + "fieldtype": "Code", + "label": "Filters", + "options": "JSON" + } + ], + "istable": 1, + "links": [], + "modified": "2020-05-14 16:45:47.196395", + "modified_by": "Administrator", + "module": "Custom", + "name": "Package Document Type", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/package_document_type/package_document_type.py b/frappe/custom/doctype/package_document_type/package_document_type.py new file mode 100644 index 0000000000..6e166eecbd --- /dev/null +++ b/frappe/custom/doctype/package_document_type/package_document_type.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 PackageDocumentType(Document): + pass diff --git a/frappe/custom/doctype/package_publish_target/__init__.py b/frappe/custom/doctype/package_publish_target/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json new file mode 100644 index 0000000000..baeb7cb8bc --- /dev/null +++ b/frappe/custom/doctype/package_publish_target/package_publish_target.json @@ -0,0 +1,47 @@ +{ + "actions": [], + "creation": "2020-05-13 16:04:32.724663", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "instance_url", + "username", + "password" + ], + "fields": [ + { + "fieldname": "instance_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Site URL", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Password", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-05-15 17:35:16.282235", + "modified_by": "Administrator", + "module": "Custom", + "name": "Package Publish Target", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.py b/frappe/custom/doctype/package_publish_target/package_publish_target.py new file mode 100644 index 0000000000..34eee02562 --- /dev/null +++ b/frappe/custom/doctype/package_publish_target/package_publish_target.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 PackagePublishTarget(Document): + pass diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js new file mode 100644 index 0000000000..a0190a8d8c --- /dev/null +++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js @@ -0,0 +1,159 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Package Publish Tool', { + refresh: function(frm) { + frm.set_query("document_type", "package_details", function () { + return { + filters: { + "istable": 0, + } + }; + }); + + frappe.realtime.on("package", (data) => { + frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message])); + if ((data.progress+1) != data.total) { + frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message])); + } else { + frm.dashboard.hide_progress(); + } + }); + + frm.trigger("show_instructions"); + frm.trigger("last_deployed_on"); + frm.trigger("set_dirty_trigger"); + frm.trigger("set_deploy_primary_action"); + }, + last_deployed_on: function(frm) { + if (frm.doc.last_deployed_on) { + frm.trigger("show_indicator"); + } + }, + show_indicator: function(frm) { + let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on); + frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue"); + }, + set_dirty_trigger: function(frm) { + $(frm.wrapper).on("dirty", function() { + frm.page.set_primary_action(__('Save'), () => frm.save()); + }); + }, + set_deploy_primary_action: function(frm) { + if (frm.doc.package_details.length && frm.doc.instances.length) { + frm.page.set_primary_action(__("Publish"), function () { + frappe.show_alert({ + message: __("Publishing documents..."), + indicator: "green" + }); + + frappe.call({ + method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package", + callback: function() { + frm.reload_doc(); + frappe.msgprint(__("Documents have been published.")); + } + }); + }); + } + }, + show_instructions: function(frm) { + let field = frm.get_field("html_info"); + field.html(` +

+ Package Publish Tool let's you copy documents from your site to any other remote site. + Follow the steps below to publish. +

+
    +
  1. Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.
  2. +
  3. Add the Sites URL where you want to copy these documents, and enter the Username and Password.
  4. +
  5. Click on Save. Now, you can click on Publish and the documents will be copied.
  6. +
+ `); + } +}); + +frappe.ui.form.on('Package Document Type', { + form_render: function (frm, cdt, cdn) { + function _show_filters(filters, table) { + table.find('tbody').empty(); + + if (filters.length > 0) { + filters.forEach(filter => { + const filter_row = + $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find('tbody').append(filter_row); + }); + } else { + const filter_row = $(` + ${__("Click to Set Filters")}`); + table.find('tbody').append(filter_row); + } + } + + let row = frappe.get_doc(cdt, cdn); + + let wrapper = $(`[data-fieldname="filters_json"]`).empty(); + let table = $(` + + + + + + + + + +
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); + $(`

${__("Click table to edit")}

`).appendTo(wrapper); + + let filters = JSON.parse(row.filters_json || '[]'); + _show_filters(filters, table); + + table.on('click', () => { + if (!row.document_type) { + frappe.msgprint(__("Select Document Type.")); + return; + } + + frappe.model.with_doctype(row.document_type, function() { + let dialog = new frappe.ui.Dialog({ + title: __('Set Filters'), + fields: [ + { + fieldtype: 'HTML', + label: 'Filters', + fieldname: 'filter_area', + } + ], + primary_action: function() { + let values = filter_group.get_filters(); + let flt = []; + if (values) { + values.forEach(function(value) { + flt.push([value[0], value[1], value[2], value[3]]); + }); + } + row.filters_json = JSON.stringify(flt); + _show_filters(flt, table); + dialog.hide(); + }, + primary_action_label: "Set" + }); + + let filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field('filter_area').$wrapper, + doctype: row.document_type, + on_change: () => {}, + }); + filter_group.add_filters_to_filter_group(filters); + dialog.show(); + }); + }); + }, +}); diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json new file mode 100644 index 0000000000..0f85ae0348 --- /dev/null +++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json @@ -0,0 +1,84 @@ +{ + "actions": [], + "creation": "2020-05-13 15:54:38.082657", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "html_info", + "sb_00", + "package_details", + "sb_01", + "instances", + "last_deployed_on" + ], + "fields": [ + { + "description": "Click on the row for accessing filters.", + "fieldname": "package_details", + "fieldtype": "Table", + "label": "Document Types", + "options": "Package Document Type", + "reqd": 1 + }, + { + "fieldname": "instances", + "fieldtype": "Table", + "label": "Sites", + "options": "Package Publish Target", + "reqd": 1 + }, + { + "fieldname": "html_info", + "fieldtype": "HTML" + }, + { + "fieldname": "last_deployed_on", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Last Deployed On", + "read_only": 1 + }, + { + "fieldname": "sb_00", + "fieldtype": "Section Break" + }, + { + "fieldname": "sb_01", + "fieldtype": "Section Break" + } + ], + "issingle": 1, + "links": [], + "modified": "2020-05-15 17:31:37.060199", + "modified_by": "Administrator", + "module": "Custom", + "name": "Package Publish Tool", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py new file mode 100644 index 0000000000..a01dd0ba47 --- /dev/null +++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +import datetime +import base64 +from frappe.model.document import Document +from frappe.utils.file_manager import save_file, get_file +from frappe import _ +from six import string_types +from frappe.frappeclient import FrappeClient +from frappe.utils import get_datetime_str, get_datetime +from frappe.utils.password import get_decrypted_password + +class PackagePublishTool(Document): + pass + +@frappe.whitelist() +def deploy_package(): + package, doc = export_package() + + file_name = "Package-" + get_datetime_str(get_datetime()) + + length = len(doc.instances) + for idx, instance in enumerate(doc.instances): + frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")}, + user=frappe.session.user) + + install_package_to_remote(package, instance) + + frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime()) + +def install_package_to_remote(package, instance): + try: + connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name)) + except Exception: + frappe.log_error(frappe.get_traceback()) + frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url)) + + try: + connection.post_request({ + "cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package", + "package": json.dumps(package) + }) + except Exception: + frappe.log_error(frappe.get_traceback()) + frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url)) + +@frappe.whitelist() +def export_package(): + """Export package as JSON.""" + package_doc = frappe.get_single("Package Publish Tool") + package = [] + + for doctype in package_doc.package_details: + filters = [] + + if doctype.get("filters_json"): + filters = json.loads(doctype.get("filters_json")) + + docs = frappe.get_all(doctype.get("document_type"), filters=filters) + length = len(docs) + + for idx, doc in enumerate(docs): + frappe.publish_realtime("package", { + "progress":idx, "total":length, + "message":doctype.get("document_type"), + "prefix": _("Exporting") + }, + user=frappe.session.user) + + document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict() + attachments = [] + + if doctype.attachments: + filters = { + "attached_to_doctype": document.get("doctype"), + "attached_to_name": document.get("name") + } + + for f in frappe.get_list("File", filters=filters): + fname, fcontents = get_file(f.name) + attachments.append({ + "fname": fname, + "content": base64.b64encode(fcontents).decode('ascii') + }) + + document.update({ + "__attachments": attachments, + "__overwrite": True if doctype.overwrite else False + }) + + package.append(document) + + return post_process(package), package_doc + +@frappe.whitelist() +def import_package(package=None): + """Import package from JSON.""" + if isinstance(package, string_types): + package = json.loads(package) + + for doc in package: + modified = doc.pop("modified") + overwrite = doc.pop("__overwrite") + attachments = doc.pop("__attachments") + exists = frappe.db.exists(doc.get("doctype"), doc.get("name")) + + if not exists: + d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True) + if attachments: + add_attachment(attachments, d) + else: + docname = doc.pop("name") + document = frappe.get_doc(doc.get("doctype"), docname) + + if overwrite: + update_document(document, doc, attachments) + + else: + if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified): + update_document(document, doc, attachments) + +def update_document(document, doc, attachments): + document.update(doc) + document.save() + if attachments: + add_attachment(attachments, document) + +def add_attachment(attachments, doc): + for attachment in attachments: + save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name")) + +def post_process(package): + """Remove the keys from Document and Child Document. Convert datetime, date, time to str.""" + del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus') + child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name') + + for doc in package: + for key in del_keys: + if key in doc: + del doc[key] + + for key, value in doc.items(): + stringified_value = get_stringified_value(value) + if stringified_value: + doc[key] = stringified_value + + if not isinstance(value, list): + continue + + for child in value: + for child_key in child_del_keys: + if child_key in child: + del child[child_key] + + for child_key, child_value in child.items(): + stringified_value = get_stringified_value(child_value) + if stringified_value: + child[child_key] = stringified_value + + return package + +def get_stringified_value(value): + if isinstance(value, datetime.datetime): + return frappe.utils.get_datetime_str(value) + + if isinstance(value, datetime.date): + return frappe.utils.get_date_str(value) + + if isinstance(value, datetime.timedelta): + return frappe.utils.get_time_str(value) + + return None diff --git a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py new file mode 100644 index 0000000000..8332240543 --- /dev/null +++ b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.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 TestPackagePublishTool(unittest.TestCase): + pass diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 46940cc846..bd93069a3f 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -63,6 +63,7 @@ CREATE TABLE `tabDocField` ( `precision` varchar(255) DEFAULT NULL, `length` int(11) NOT NULL DEFAULT 0, `translatable` int(1) NOT NULL DEFAULT 0, + `hide_border` int(1) NOT NULL DEFAULT 0, PRIMARY KEY (`name`), KEY `parent` (`parent`), KEY `label` (`label`), diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 26760dbcc9..76309e7347 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -63,6 +63,7 @@ CREATE TABLE "tabDocField" ( "precision" varchar(255) DEFAULT NULL, "length" bigint NOT NULL DEFAULT 0, "translatable" smallint NOT NULL DEFAULT 0, + "hide_border" smallint NOT NULL DEFAULT 0, PRIMARY KEY ("name") ) ; diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ddcfb670d4..512b3f2890 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -127,6 +127,8 @@ class Workspace: return name in self.allowed_reports if item_type == "help": return True + if item_type == "dashboard": + return True return False @@ -272,6 +274,8 @@ class Workspace: for doc in self.onboarding_doc.get_steps(): step = doc.as_dict().copy() step.label = _(doc.title) + if step.action == "Create Entry": + step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True) steps.append(step) return steps diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index c17bc3235c..c0e2bddcf8 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -9,6 +9,7 @@ "dashboard_name", "is_default", "charts", + "chart_options", "cards" ], "fields": [ @@ -33,6 +34,13 @@ "options": "Dashboard Chart Link", "reqd": 1 }, + { + "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])", + "fieldname": "chart_options", + "fieldtype": "Code", + "label": "Chart Options", + "options": "JSON" + }, { "fieldname": "cards", "fieldtype": "Table", @@ -41,7 +49,7 @@ } ], "links": [], - "modified": "2020-04-19 17:44:36.237163", + "modified": "2020-04-29 13:26:37.362482", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index b85e135071..af0c48d9c6 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals from frappe.model.document import Document import frappe +from frappe import _ +import json class Dashboard(Document): def on_update(self): @@ -13,13 +15,29 @@ class Dashboard(Document): frappe.db.sql('''update tabDashboard set is_default = 0 where name != %s''', self.name) + def validate(self): + self.validate_custom_options() + + def validate_custom_options(self): + if self.chart_options: + try: + json.loads(self.chart_options) + except ValueError as error: + frappe.throw(_("Invalid json added in the custom options: {0}").format(error)) + @frappe.whitelist() def get_permitted_charts(dashboard_name): permitted_charts = [] dashboard = frappe.get_doc('Dashboard', dashboard_name) for chart in dashboard.charts: if frappe.has_permission('Dashboard Chart', doc=chart.chart): - permitted_charts.append(chart) + chart_dict = frappe._dict() + chart_dict.update(chart.as_dict()) + + if dashboard.get('chart_options'): + chart_dict.custom_options = dashboard.get('chart_options') + permitted_charts.append(chart_dict) + return permitted_charts @frappe.whitelist() diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index f8d5886b26..2ec73cff42 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -49,6 +49,7 @@ frappe.ui.form.on('Dashboard Chart', { }); frm.set_df_property("filters_section", "hidden", 1); + frm.trigger('set_time_series'); frm.set_query('document_type', function() { return { filters: { @@ -57,6 +58,7 @@ frappe.ui.form.on('Dashboard Chart', { } }); frm.trigger('update_options'); + frm.trigger('set_heatmap_year_options'); if (frm.doc.report_name) { frm.trigger('set_chart_report_filters'); } @@ -70,7 +72,17 @@ frappe.ui.form.on('Dashboard Chart', { frm.trigger("show_filters"); }, + set_heatmap_year_options: function(frm) { + if (frm.doc.type == 'Heatmap') { + frappe.db.get_doc('System Settings').then(doc => { + const creation_date = doc.creation; + frm.set_df_property('heatmap_year', 'options', frappe.dashboard_utils.get_years_since_creation(creation_date)); + }); + } + }, + chart_type: function(frm) { + frm.trigger('set_time_series'); if (frm.doc.chart_type == 'Report') { frm.set_query('report_name', () => { return { @@ -80,23 +92,25 @@ frappe.ui.form.on('Dashboard Chart', { } }); } else { - // set timeseries based on chart type - if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) { - frm.set_value('timeseries', 1); - } else { - frm.set_value('timeseries', 0); - } - if (frm.doc.chart_type == 'Group By') { - frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie']); + frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie', 'Donut']); } else { - frm.set_df_property('type', 'options', ['Line', 'Bar']); + frm.set_df_property('type', 'options', ['Line', 'Bar', 'Heatmap']); } frm.set_value('document_type', ''); } }, + set_time_series: function(frm) { + // set timeseries based on chart type + if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) { + frm.set_value('timeseries', 1); + } else { + frm.set_value('timeseries', 0); + } + }, + document_type: function(frm) { // update `based_on` options based on date / datetime fields frm.set_value('source', ''); @@ -283,17 +297,7 @@ frappe.ui.form.on('Dashboard Chart', { }); } } else if (frm.chart_filters.length) { - fields = frm.chart_filters.filter(f => { - if (f.on_change && !f.reqd) { - return false; - } - if (f.get_query || f.get_data) { - f.read_only = 1; - } - - return f.fieldname; - }); - + fields = frm.chart_filters.filter(f => f.fieldname); fields.map( f => { if (filters[f.fieldname]) { let condition = '='; @@ -353,10 +357,10 @@ frappe.ui.form.on('Dashboard Chart', { } dialog.show(); + //Set query report object so that it can be used while fetching filter values in the report + frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); dialog.set_values(filters); }); }, }); - - diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index b5201a8b1f..72f5c43316 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -23,17 +23,18 @@ "number_of_groups", "column_break_6", "is_public", + "heatmap_year", "timespan", "from_date", "to_date", "time_interval", "timeseries", + "type", "filters_section", "filters_json", "chart_options_section", - "type", - "column_break_2", "color", + "column_break_2", "custom_options", "section_break_10", "last_synced_on" @@ -85,14 +86,14 @@ "fieldtype": "Column Break" }, { - "depends_on": "timeseries", + "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'", "fieldname": "timespan", "fieldtype": "Select", "label": "Timespan", "options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range" }, { - "depends_on": "timeseries", + "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'", "fieldname": "time_interval", "fieldtype": "Select", "label": "Time Interval", @@ -100,7 +101,7 @@ }, { "default": "0", - "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)", + "depends_on": "eval: !['Group By', 'Report'].includes(doc.chart_type)\n", "fieldname": "timeseries", "fieldtype": "Check", "label": "Time Series" @@ -123,10 +124,11 @@ "label": "Chart Options" }, { + "default": "Line", "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Line\nBar\nPercentage\nPie\nDonut", + "options": "Line\nBar\nHeatmap", "reqd": 1 }, { @@ -134,7 +136,7 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.chart_type !== 'Report'", + "depends_on": "eval: doc.chart_type !== 'Report' && doc.type !== 'Heatmap'", "fieldname": "color", "fieldtype": "Color", "label": "Color" @@ -217,7 +219,7 @@ "options": "Dashboard Chart Field" }, { - "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]", + "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"] (the options set here will override the chart options set in the Dashboard)", "fieldname": "custom_options", "fieldtype": "Code", "label": "Custom Options" @@ -228,10 +230,16 @@ "fieldname": "is_public", "fieldtype": "Check", "label": "Is Public" + }, + { + "depends_on": "eval: doc.type == 'Heatmap'", + "fieldname": "heatmap_year", + "fieldtype": "Select", + "label": "Year" } ], "links": [], - "modified": "2020-05-01 15:22:59.119341", + "modified": "2020-05-01 19:45:01.669384", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", @@ -275,4 +283,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 417ef2ba82..7e375e835f 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -8,7 +8,7 @@ from frappe import _ import datetime import json from frappe.utils.dashboard import cache_source, get_from_date_from_timespan -from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime +from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime, cint from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports from frappe.model.document import Document @@ -58,13 +58,13 @@ def has_permission(doc, ptype, user): @frappe.whitelist() @cache_source def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, refresh = None): + to_date = None, timespan = None, time_interval = None, heatmap_year=None, refresh = None): if chart_name: chart = frappe.get_doc('Dashboard Chart', chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) - + heatmap_year = heatmap_year or chart.heatmap_year timespan = timespan or chart.timespan if timespan == 'Select Date Range': @@ -87,7 +87,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d if chart.chart_type == 'Group By': chart_config = get_group_by_chart_config(chart, filters) else: - chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date) + if chart.type == 'Heatmap': + chart_config = get_heatmap_chart_config(chart, filters, heatmap_year) + else: + chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date) return chart_config @@ -174,6 +177,41 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): return chart_config +def get_heatmap_chart_config(chart, filters, heatmap_year): + aggregate_function = get_aggregate_function(chart.chart_type) + value_field = chart.value_based_on or '1' + doctype = chart.document_type + datefield = chart.based_on + year = cint(heatmap_year) if heatmap_year else getdate(nowdate()).year + year_start_date = datetime.date(year, 1, 1).strftime('%Y-%m-%d') + next_year_start_date = datetime.date(year + 1, 1, 1).strftime('%Y-%m-%d') + + filters.append([doctype, datefield, '>', "{date}".format(date=year_start_date), False]) + filters.append([doctype, datefield, '<', "{date}".format(date=next_year_start_date), False]) + + if frappe.db.db_type == 'mariadb': + timestamp_field = 'unix_timestamp({datefield})'.format(datefield=datefield) + else: + timestamp_field = 'extract(epoch from timestamp {datefield})'.format(datefield=datefield) + + data = dict(frappe.db.get_all( + doctype, + fields = [ + timestamp_field, + '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field), + ], + filters = filters, + group_by = 'date({datefield})'.format(datefield=datefield), + as_list = 1, + order_by = '{datefield} asc'.format(datefield=datefield), + ignore_ifnull = True + )) + + chart_config = { + 'labels': [], + 'dataPoints': data, + } + return chart_config def get_group_by_chart_config(chart, filters): @@ -397,11 +435,11 @@ class DashboardChart(Document): def check_document_type(self): if frappe.get_meta(self.document_type).issingle: - frappe.throw("You cannot create a dashboard chart from single DocTypes") + frappe.throw(_("You cannot create a dashboard chart from single DocTypes")) def validate_custom_options(self): if self.custom_options: try: json.loads(self.custom_options) except ValueError as error: - frappe.throw("Invalid json added in the custom options: %s" % error) \ No newline at end of file + frappe.throw(_("Invalid json added in the custom options: {0}").format(error)) diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js index ec8eaaa60b..503859eb61 100644 --- a/frappe/desk/doctype/desk_page/desk_page.js +++ b/frappe/desk/doctype/desk_page/desk_page.js @@ -12,7 +12,7 @@ frappe.ui.form.on('Desk Page', { frm.set_df_property("extends", "read_only", true); } - if (frm.doc.for_user || frm.doc.is_standard) { + if (frm.doc.for_user || (frm.doc.is_standard && !frappe.boot.developer_mode)) { frm.trigger('disable_form'); } }, diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json index 550ea609c8..f3fd546a77 100644 --- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json +++ b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json @@ -23,7 +23,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Type", - "options": "DocType\nReport\nPage", + "options": "DocType\nReport\nPage\nDashboard", "reqd": 1 }, { @@ -88,7 +88,7 @@ ], "istable": 1, "links": [], - "modified": "2020-05-13 19:26:34.229669", + "modified": "2020-05-14 16:02:15.420993", "modified_by": "Administrator", "module": "Desk", "name": "Desk Shortcut", diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json index 3bc9d5f286..37d1d63dbe 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.json +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json @@ -15,11 +15,16 @@ "action", "column_break_7", "reference_document", + "show_full_form", "is_single", "reference_report", "report_reference_doctype", "report_type", "report_description", + "path", + "callback_title", + "callback_message", + "validate_action", "field", "value_to_validate", "video_url" @@ -58,7 +63,7 @@ "fieldname": "action", "fieldtype": "Select", "label": "Action", - "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nWatch Video", + "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video", "reqd": 1 }, { @@ -70,6 +75,7 @@ "fieldname": "reference_document", "fieldtype": "Link", "label": "Reference Document", + "mandatory_depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"", "options": "DocType" }, { @@ -84,7 +90,8 @@ "depends_on": "eval:doc.action == \"Watch Video\"", "fieldname": "video_url", "fieldtype": "Data", - "label": "Video URL" + "label": "Video URL", + "mandatory_depends_on": "eval:doc.action == \"Watch Video\"" }, { "depends_on": "eval:doc.action == \"View Report\"", @@ -102,17 +109,19 @@ "label": "Is Skipped" }, { - "depends_on": "eval:doc.action == \"Update Settings\"", + "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action", "fieldname": "field", "fieldtype": "Select", - "label": "Field" + "label": "Field", + "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action" }, { - "depends_on": "eval:doc.action == \"Update Settings\"", + "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action", "description": "Use % for any non empty value.", "fieldname": "value_to_validate", "fieldtype": "Data", - "label": "Value to Validate" + "label": "Value to Validate", + "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action" }, { "depends_on": "eval:doc.action == \"View Report\"", @@ -136,10 +145,46 @@ "fieldname": "is_single", "fieldtype": "Check", "label": "Is Single" + }, + { + "depends_on": "eval:doc.action == \"Go to Page\"", + "description": "Example: #Tree/Account", + "fieldname": "path", + "fieldtype": "Data", + "label": "Path", + "mandatory_depends_on": "eval:doc.action == \"Go to Page\"" + }, + { + "depends_on": "eval:doc.action == \"Go to Page\"", + "fieldname": "callback_title", + "fieldtype": "Data", + "label": "Callback Title" + }, + { + "depends_on": "eval:doc.action == \"Go to Page\"", + "description": "This will be shown in a modal after routing", + "fieldname": "callback_message", + "fieldtype": "Small Text", + "label": "Callback Message" + }, + { + "default": "1", + "depends_on": "eval:doc.action == \"Update Settings\"", + "fieldname": "validate_action", + "fieldtype": "Check", + "label": "Validate Field" + }, + { + "default": "0", + "depends_on": "eval:doc.action == \"Create Entry\"", + "description": "Show full form instead of a quick entry modal", + "fieldname": "show_full_form", + "fieldtype": "Check", + "label": "Show Full Form?" } ], "links": [], - "modified": "2020-05-11 13:24:05.457160", + "modified": "2020-05-14 15:10:05.627706", "modified_by": "Administrator", "module": "Desk", "name": "Onboarding Step", diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index e1cc5dfba4..8086acbb2a 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -10,3 +10,7 @@ class OnboardingStep(Document): def before_export(self, doc): doc.is_complete = 0 doc.is_skipped = 0 + + def validate(self): + if self.action == "Go to Page": + self.is_mandatory = 0 diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 109dd25f4f..4a1302788b 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -212,7 +212,10 @@ def get_notification_config(): def get_filters_for(doctype): '''get open filters for doctype''' config = get_notification_config() - return config.get("for_doctype").get(doctype, {}) + doctype_config = config.get("for_doctype").get(doctype, {}) + filters = doctype_config if not isinstance(doctype_config, string_types) else None + + return filters @frappe.whitelist() @frappe.read_only() diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index 6917ef0426..60e1f3242a 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -14,6 +14,7 @@ def install(): update_global_search_doctypes() setup_email_linking() sync_dashboards() + add_unsubscribe() @frappe.whitelist() def update_genders(): @@ -37,3 +38,15 @@ def setup_email_linking(): "email_id": "email_linking@example.com", }) doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + +def add_unsubscribe(): + email_unsubscribe = [ + {"email": "admin@example.com", "global_unsubscribe": 1}, + {"email": "guest@example.com", "global_unsubscribe": 1} + ] + + for unsubscribe in email_unsubscribe: + if not frappe.get_all("Email Unsubscribe", filters=unsubscribe): + doc = frappe.new_doc("Email Unsubscribe") + doc.update(unsubscribe) + doc.insert(ignore_permissions=True) diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index ff1e906cff..c43ff27ba3 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -108,21 +108,6 @@ class UserProfile { }); } - get_years_since_creation() { - //Get years since user account created - this.user_creation = frappe.boot.user.creation; - let creation_year = this.get_year(this.user_creation); - let current_year = this.get_year(frappe.datetime.now_date()); - let years_list = []; - for (var year = current_year; year >= creation_year; year--) { - years_list.push(year); - } - return years_list; - } - - get_year(date_str) { - return date_str.substring(0, date_str.indexOf('-')); - } render_line_chart() { this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]]; @@ -246,8 +231,8 @@ class UserProfile { create_heatmap_chart_filters() { let filters = [ { - label: this.get_year(frappe.datetime.now_date()), - options: this.get_years_since_creation(), + label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()), + options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation), action: (selected_item) => { this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item)); } diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py new file mode 100644 index 0000000000..0b689478d2 --- /dev/null +++ b/frappe/integrations/frappe_providers/__init__.py @@ -0,0 +1,14 @@ +# imports - standard imports +import sys + +# imports - module imports +from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrator + + +def migrate_to(local_site, frappe_provider): + if frappe_provider in ("frappe.cloud", "frappecloud.com"): + frappe_provider = "frappecloud.com" + return frappecloud_migrator(local_site, frappe_provider) + else: + print("{} is not supported yet".format(frappe_provider)) + sys.exit(1) diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py new file mode 100644 index 0000000000..4f33c990f9 --- /dev/null +++ b/frappe/integrations/frappe_providers/frappecloud.py @@ -0,0 +1,268 @@ +# imports - standard imports +import getpass +import json +import re +import sys + +# imports - third party imports +import click +from html2text import html2text +import requests + +# imports - module imports +import frappe +import frappe.utils.backups +from frappe.utils import get_installed_apps_info +from frappe.utils.commands import render_table, add_line_after + + +def get_new_site_options(): + site_options_sc = session.post(options_url) + + if site_options_sc.ok: + site_options = site_options_sc.json()["message"] + return site_options + else: + print("Couldn't retrive New site information: {}".format(site_options_sc.status_code)) + + +def is_valid_subdomain(subdomain): + if len(subdomain) < 5: + print("Subdomain too short. Use 5 or more characters") + return False + matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain) + if matched: + return True + print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens") + + +def is_subdomain_available(subdomain): + res = session.post(site_exists_url, {"subdomain": subdomain}) + if res.ok: + available = not res.json()["message"] + if not available: + print("Subdomain already exists! Try another one") + + return available + + +def render_plan_table(plans_list): + plans_table = [] + + # title row + visible_headers = ["name", "cpu_time_per_day"] + plans_table.append(["Plan", "CPU Time"]) + + # all rows + for plan in plans_list: + plan, cpu_time = [plan[header] for header in visible_headers] + plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")]) + + render_table(plans_table) + + +@add_line_after +def choose_plan(plans_list): + print("{} plans available".format(len(plans_list))) + available_plans = [plan["name"] for plan in plans_list] + render_plan_table(plans_list) + + while True: + input_plan = click.prompt("Select Plan").strip() + if input_plan in available_plans: + print("{} Plan selected ✅".format(input_plan)) + return input_plan + else: + print("Invalid Selection ❌") + + +@add_line_after +def check_app_compat(available_group): + is_compat = True + incompatible_apps, filtered_apps, branch_msgs = [], [], [] + existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()] + print("Checking availability of existing app group") + + for (app, branch) in existing_group: + info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ] + if info: + app_title, available_branch = info[0] + + if branch != available_branch: + print("⚠️ App {}:{} => {}".format(app, branch, available_branch)) + branch_msgs.append([app, branch, available_branch]) + filtered_apps.append(app_title) + is_compat = False + + else: + print("✅ App {}:{}".format(app, branch)) + filtered_apps.append(app_title) + + else: + incompatible_apps.append(app) + print("❌ App {}:{}".format(app, branch)) + is_compat = False + + start_msg = "\nSelecting this group will " + incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else "" + branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else "" + changes = (incompatible_apps + branch_change) or "be perfect for you :)" + warning_message = start_msg + changes + print(warning_message) + + return is_compat, filtered_apps + + +def render_group_table(app_groups): + # title row + app_groups_table = [["#", "App Group", "Apps"]] + + # all rows + for idx, app_group in enumerate(app_groups): + apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]]) + row = [idx + 1, app_group["name"], apps_list] + app_groups_table.append(row) + + render_table(app_groups_table) + + +@add_line_after +def filter_apps(app_groups): + render_group_table(app_groups) + + while True: + app_group_index = click.prompt("Select App Group Number", type=int) - 1 + try: + if app_group_index == -1: + raise IndexError + selected_group = app_groups[app_group_index] + except IndexError: + print("Invalid Selection ❌") + continue + + is_compat, filtered_apps = check_app_compat(selected_group) + + if is_compat or click.confirm("Continue anyway?"): + print("App Group {} selected! ✅".format(selected_group["name"])) + break + + return selected_group["name"], filtered_apps + +@add_line_after +def create_session(): + # take user input from STDIN + username = click.prompt("Username").strip() + password = getpass.unix_getpass() + + auth_credentials = {"usr": username, "pwd": password} + + session = requests.Session() + login_sc = session.post(login_url, auth_credentials) + + if login_sc.ok: + print("Authorization Successful! ✅") + session.headers.update({"X-Press-Team": username}) + return session + else: + print("Authorization Failed with Error Code {}".format(login_sc.status_code)) + + +@add_line_after +def get_subdomain(domain): + while True: + subdomain = click.prompt("Enter subdomain").strip() + if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain): + print("Site Domain: {}.{}".format(subdomain, domain)) + return subdomain + + +@add_line_after +def upload_backup(local_site): + # take backup + files_session = {} + print("Taking backup for site {}".format(local_site)) + odb = frappe.utils.backups.new_backup(ignore_files=False, force=True) + + # upload files + for x, (file_type, file_path) in enumerate([ + ("database", odb.backup_path_db), + ("public", odb.backup_path_files), + ("private", odb.backup_path_private_files) + ]): + file_upload_response = session.post(files_url, data={}, files={ + "file": open(file_path, "rb"), + "is_private": 1, + "folder": "Home", + "method": "press.api.site.upload_backup", + "type": file_type + }) + print("Uploading files ({}/3)".format(x+1), end="\r") + if file_upload_response.ok: + files_session[file_type] = file_upload_response.json()["message"] + else: + print("Upload failed for: {}".format(file_path)) + + files_uploaded = { k: v["file_url"] for k, v in files_session.items() } + print("Uploaded backup files! ✅") + + return files_uploaded + + +def frappecloud_migrator(local_site, remote_site): + global login_url, upload_url, files_url, options_url, site_exists_url, session + + login_url = "https://{}/api/method/login".format(remote_site) + upload_url = "https://{}/api/method/press.api.site.new".format(remote_site) + files_url = "https://{}/api/method/upload_file".format(remote_site) + options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site) + site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site) + + print("Frappe Cloud credentials @ {}".format(remote_site)) + + # get credentials + auth user + start session + session = create_session() + + if session: + # connect to site db + frappe.init(site=local_site) + frappe.connect() + + # get new site options + site_options = get_new_site_options() + + # set preferences from site options + subdomain = get_subdomain(site_options["domain"]) + plan = choose_plan(site_options["plans"]) + + app_groups = site_options["groups"] + selected_group, filtered_apps = filter_apps(app_groups) + files_uploaded = upload_backup(local_site) + + # push to frappe_cloud + payload = json.dumps({ + "site": { + "apps": filtered_apps, + "files": files_uploaded, + "group": selected_group, + "name": subdomain, + "plan": plan + } + }) + + session.headers.update({"Content-Type": "application/json; charset=utf-8"}) + site_creation_request = session.post(upload_url, payload) + frappe.destroy() + + if site_creation_request.ok: + site_url = site_creation_request.json()["message"] + print("Your site {} is being migrated ✨".format(local_site)) + print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url)) + print("Your site URL: {}".format(site_url)) + else: + print("Request failed with error code {}".format(site_creation_request.status_code)) + reason = html2text(site_creation_request.text) + print(reason) + sys.exit(1) + + else: + sys.exit(1) diff --git a/frappe/migrate.py b/frappe/migrate.py index 094abbe099..9ec23d8ae7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -5,11 +5,13 @@ from __future__ import unicode_literals import json import os +import sys import frappe import frappe.translate import frappe.modules.patch_handler import frappe.model.sync from frappe.utils.fixtures import sync_fixtures +from frappe.utils.connections import check_connection from frappe.utils.dashboard import sync_dashboards from frappe.cache_manager import clear_global_cache from frappe.desk.notifications import clear_notifications @@ -19,6 +21,7 @@ from frappe.modules.utils import sync_customizations from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.utils import global_search + def migrate(verbose=True, rebuild_website=False, skip_failing=False): '''Migrate all apps to the latest version, will: - run before migrate hooks @@ -32,6 +35,19 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False): - run after migrate hooks ''' + service_status = check_connection(redis_services=["redis_cache"]) + if False in service_status.values(): + for service in service_status: + if not service_status.get(service, True): + print("{} service is not running.".format(service)) + print("""Cannot run bench migrate without the services running. +If you are running bench in development mode, make sure that bench is running: + +$ bench start + +Otherwise, check the server logs and ensure that all the required services are running.""") + sys.exit(1) + touched_tables_file = frappe.get_site_path('touched_tables.json') if os.path.exists(touched_tables_file): os.remove(touched_tables_file) @@ -67,6 +83,9 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False): # add static pages to global search global_search.update_global_search_for_all_web_pages() + # updating installed applications data + frappe.get_single('Installed Applications').update_versions() + #run after_migrate hooks for app in frappe.get_installed_apps(): for fn in frappe.get_hooks('after_migrate', app_name=app): diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 7bf93d1968..c8fd1a2ac2 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -443,34 +443,41 @@ class Meta(Document): def add_doctype_links(self, data): '''add `links` child table in standard link dashboard format''' + dashboard_links = [] + if hasattr(self, 'links') and self.links: - if not data.transactions: - # init groups - data.transactions = [] - data.non_standard_fieldnames = {} + dashboard_links.extend(self.links) - for link in self.links: - link.added = False - for group in data.transactions: - group = frappe._dict(group) - # group found - if link.group and group.label == link.group: - if link.link_doctype not in group.get('items'): - group.get('items').append(link.link_doctype) - link.added = True + if frappe.get_all("Custom Link", {"document_type": self.name}): + dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links) - if not link.added: - # group not found, make a new group - data.transactions.append(dict( - label = link.group, - items = [link.link_doctype] - )) + if not data.transactions: + # init groups + data.transactions = [] + data.non_standard_fieldnames = {} - if link.link_fieldname != data.fieldname: - if data.fieldname: - data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname - else: - data.fieldname = link.link_fieldname + for link in dashboard_links: + link.added = False + for group in data.transactions: + group = frappe._dict(group) + # group found + if link.group and group.label == link.group: + if link.link_doctype not in group.get('items'): + group.get('items').append(link.link_doctype) + link.added = True + + if not link.added: + # group not found, make a new group + data.transactions.append(dict( + label = link.group, + items = [link.link_doctype] + )) + + if link.link_fieldname != data.fieldname: + if data.fieldname: + data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname + else: + data.fieldname = link.link_fieldname def get_row_template(self): diff --git a/frappe/patches.txt b/frappe/patches.txt index 86491b32b4..8ab9418e6c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -280,3 +280,4 @@ frappe.patches.v13_0.set_read_times frappe.patches.v13_0.remove_web_view frappe.patches.v13_0.remove_tailwind_from_page_builder frappe.patches.v13_0.rename_onboarding +frappe.patches.v13_0.email_unsubscribe diff --git a/frappe/patches/v13_0/email_unsubscribe.py b/frappe/patches/v13_0/email_unsubscribe.py new file mode 100644 index 0000000000..69ed1be948 --- /dev/null +++ b/frappe/patches/v13_0/email_unsubscribe.py @@ -0,0 +1,13 @@ +import frappe + +def execute(): + email_unsubscribe = [ + {"email": "admin@example.com", "global_unsubscribe": 1}, + {"email": "guest@example.com", "global_unsubscribe": 1} + ] + + for unsubscribe in email_unsubscribe: + if not frappe.get_all("Email Unsubscribe", filters=unsubscribe): + doc = frappe.new_doc("Email Unsubscribe") + doc.update(unsubscribe) + doc.insert(ignore_permissions=True) \ No newline at end of file diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index f54b9e5cbe..6b723d508c 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -2259,14 +2259,19 @@ class extends Component { ) : null, h("div","", h("div", { class: "panel-title" }, - h("div", { class: "cursor-pointer", onclick: () => { frappe.set_route(item.route) }}, + h("div", { class: "cursor-pointer", onclick: () => { + frappe.session.user !== "Guest" ? + frappe.set_route(item.route) : null; + }}, h(frappe.Chat.Widget.MediaProfile, { ...item }) ) ) ), - h("div", { class: popper ? "col-xs-1" : "col-xs-3" }, + h("div", { class: popper ? "col-xs-2" : "col-xs-3" }, h("div", { class: "text-right" }, - + frappe._.is_mobile() && h(frappe.components.Button, { class: "frappe-chat-close", onclick: props.toggle }, + h(frappe.components.Octicon, { type: "x" }) + ) ) ) ) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 01dfbf81f9..bad7c877fc 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -651,6 +651,12 @@ frappe.ui.form.Form = class FrappeForm { callback && callback(); me.script_manager.trigger("on_submit") .then(() => resolve(me)); + if (frappe.route_hooks.after_submit) { + let route_callback = frappe.route_hooks.after_submit; + delete frappe.route_hooks.after_submit; + + route_callback(me); + } } }, btn, () => me.handle_save_fail(btn, on_error), resolve); }); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 5aeb29b1ed..d6106255a0 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -599,12 +599,15 @@ frappe.ui.form.Section = Class.extend({ if(this.df.cssClass) { this.wrapper.addClass(this.df.cssClass); } + if (this.df.hide_border) { + this.wrapper.toggleClass("hide-border", true); + } } - // for bc this.body = $('
').appendTo(this.wrapper); }, + make_head: function() { var me = this; if(!this.df.collapsible) { @@ -663,9 +666,11 @@ frappe.ui.form.Section = Class.extend({ } }); }, + is_collapsed() { return this.body.hasClass('hide'); }, + has_missing_mandatory: function() { var missing_mandatory = false; for (var j=0, l=this.fields_list.length; j < l; j++) { diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 9996389a4e..68444c8a3b 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -107,7 +107,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ }); this.register_primary_action(); - this.render_edit_in_full_page_link(); + !this.force && this.render_edit_in_full_page_link(); // ctrl+enter to save this.dialog.wrapper.keydown(function(e) { if((e.ctrlKey || e.metaKey) && e.which==13) { @@ -213,8 +213,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({ me.dialog.doc = r.message; if (frappe._from_link) { frappe.ui.form.update_calling_link(me.dialog.doc); + } else { + if (me.after_insert) { + me.after_insert(me.dialog.doc); + } else { + me.open_form_if_not_list(); + } } - cur_frm.reload_doc(); + + cur_frm && cur_frm.reload_doc(); } }); }, diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index a1628be34a..d1621a3e15 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -82,5 +82,21 @@ frappe.dashboard_utils = { ).then(settings => { return settings; }); + }, + + get_years_since_creation(creation) { + //Get years since user account created + let creation_year = this.get_year(creation); + let current_year = this.get_year(frappe.datetime.now_date()); + let years_list = []; + for (var year = current_year; year >= creation_year; year--) { + years_list.push(year); + } + return years_list; + }, + + get_year(date_str) { + return date_str.substring(0, date_str.indexOf('-')); } + }; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js index ef235ed3b1..7618d58829 100644 --- a/frappe/public/js/frappe/utils/pretty_date.js +++ b/frappe/public/js/frappe/utils/pretty_date.js @@ -76,6 +76,7 @@ window.comment_when = function(datetime, mini) { + prettyDate(datetime, mini) + ''; }; frappe.datetime.comment_when = comment_when; +frappe.datetime.prettyDate = prettyDate; frappe.datetime.refresh_when = function() { if (jQuery) { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 68628276a8..3cbbecea83 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -250,7 +250,8 @@ Object.assign(frappe.utils, { regExp = /^\w+$/; break; case "email": - regExp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; + // from https://emailregex.com/ + regExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; break; case "url": regExp = /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 5956a6310d..51add61f07 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -294,7 +294,7 @@ class DesktopPage { make_charts() { return frappe.dashboard_utils.get_dashboard_settings().then(settings => { - let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {}; + let chart_config = settings.chart_config ? JSON.parse(settings.chart_config): {}; if (this.data.charts.items) { this.data.charts.items.map(chart => { chart.chart_settings = chart_config[chart.chart_name] || {}; @@ -306,6 +306,7 @@ class DesktopPage { container: this.page, type: "chart", columns: 1, + hidden: Boolean(this.onboarding_widget), options: { allow_sorting: this.allow_customization, allow_create: this.allow_customization, diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 5105494862..e79e43ae02 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -330,8 +330,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { evaluate_depends_on_value(expression, filter_label) { let out = null; - let filters = this.get_filter_values(); - if (filters) { + let doc = this.get_filter_values(); + if (doc) { if (typeof expression === 'boolean') { out = expression; } else if (expression.substr(0, 5) == 'eval:') { @@ -341,7 +341,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`)); } } else { - var value = filters[expression]; + var value = doc[expression]; if ($.isArray(value)) { out = !!value.length; } else { diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index a8149b9134..7b1205482f 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -20,7 +20,7 @@ frappe.report_utils = { return { data: { - labels: labels, + labels: labels.length? labels: null, datasets: datasets }, truncateLegends: 1, diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 856061f1f0..f7513611d1 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1020,7 +1020,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { name: __('Totals Row'), content: totals[col.id], format: value => { - return frappe.format(value, col.docfield, { always_show_decimals: true }); + return frappe.format(value, col.docfield, { always_show_decimals: true }, data[0]); } } }) diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index a50acfcd9d..e5378cf2ab 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -40,6 +40,10 @@ export default class ChartWidget extends Widget { setup_container() { this.body.empty(); + if (this.chart_doc.type == 'Heatmap') { + this.setup_heatmap_container(); + } + this.loading = $( `
${__( "Loading..." @@ -57,9 +61,16 @@ export default class ChartWidget extends Widget { this.chart_wrapper = $(`
`); this.chart_wrapper.appendTo(this.body); + this.$heatmap_legend = null; this.set_chart_title(); } + setup_heatmap_container() { + this.widget.addClass('heatmap-chart'); + this.widget.removeClass('full-width').addClass('full-width'); + this.width = 'Full'; + } + set_summary() { if (!this.$summary) { this.$summary = $(`
`).hide(); @@ -104,54 +115,7 @@ export default class ChartWidget extends Widget { } render_time_series_filters() { - let filters = [ - { - label: this.chart_settings.timespan || this.chart_doc.timespan, - options: [ - "Select Date Range", - "Last Year", - "Last Quarter", - "Last Month", - "Last Week" - ], - action: selected_item => { - this.selected_timespan = selected_item; - - if (this.selected_timespan === "Select Date Range") { - this.render_date_range_fields(); - } else { - this.selected_from_date = null; - this.selected_to_date = null; - if (this.date_field_wrapper) { - this.date_field_wrapper.hide(); - - // Title maybe hidden becuase of date range fields - // in half width chart - this.title_field.show(); - this.head.css('flex-direction', "row"); - } - - this.save_chart_config_for_user({ - 'timespan': this.selected_timespan, - 'from_date': null, - 'to_date': null - - }); - this.fetch_and_update_chart(); - } - } - }, - { - label: this.chart_settings.time_interval || this.chart_doc.time_interval, - options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"], - action: selected_item => { - this.selected_time_interval = selected_item; - this.save_chart_config_for_user({'time_interval': this.selected_time_interval}); - this.fetch_and_update_chart(); - } - } - ]; - + let filters = this.get_time_series_filters(); frappe.dashboard_utils.render_chart_filters( filters, "chart-actions", @@ -160,12 +124,77 @@ export default class ChartWidget extends Widget { ); } + get_time_series_filters() { + let filters; + if (this.chart_doc.type == 'Heatmap') { + filters = [{ + label: this.chart_settings.heatmap_year || this.chart_doc.heatmap_year, + options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation), + action: selected_item => { + this.selected_heatmap_year = selected_item; + this.save_chart_config_for_user({'heatmap_year': this.selected_heatmap_year}); + this.fetch_and_update_chart(); + } + }]; + } else { + filters = [ + { + label: this.chart_settings.timespan || this.chart_doc.timespan, + options: [ + "Select Date Range", + "Last Year", + "Last Quarter", + "Last Month", + "Last Week" + ], + action: selected_item => { + this.selected_timespan = selected_item; + + if (this.selected_timespan === "Select Date Range") { + this.render_date_range_fields(); + } else { + this.selected_from_date = null; + this.selected_to_date = null; + if (this.date_field_wrapper) { + this.date_field_wrapper.hide(); + + // Title maybe hidden becuase of date range fields + // in half width chart + this.title_field.show(); + this.head.css('flex-direction', "row"); + } + + this.save_chart_config_for_user({ + 'timespan': this.selected_timespan, + 'from_date': null, + 'to_date': null + + }); + this.fetch_and_update_chart(); + } + } + }, + { + label: this.chart_settings.time_interval || this.chart_doc.time_interval, + options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"], + action: selected_item => { + this.selected_time_interval = selected_item; + this.save_chart_config_for_user({'time_interval': this.selected_time_interval}); + this.fetch_and_update_chart(); + } + } + ]; + } + return filters; + } + fetch_and_update_chart() { this.args = { timespan: this.selected_timespan || this.chart_settings.timespan, time_interval: this.selected_time_interval || this.chart_settings.time_interval, from_date: this.selected_from_date || this.chart_settings.from_date, - to_date: this.selected_to_date || this.chart_settings.to_date + to_date: this.selected_to_date || this.chart_settings.to_date, + heatmap_year: this.selected_heatmap_year || this.chart_settings.heatmap_year, }; this.fetch(this.filters, true, this.args).then(data => { @@ -274,7 +303,7 @@ export default class ChartWidget extends Widget { }, { label: __("Reset Chart"), - action: "action-list", + action: "action-reset", handler: () => { this.reset_chart(); delete this.dashboard_chart; @@ -332,15 +361,12 @@ export default class ChartWidget extends Widget { } ]; } else { - fields = filters.filter(f => { - if (f.on_change && !f.reqd) { - return false; - } - if (f.get_query || f.get_data) { - f.read_only = 1; - } - return f.fieldname; - }); + fields = filters + .filter(df => df.fieldname) + .map(df => { + Object.assign(df, df.dashboard_config || {}); + return df; + }); } } else { fields = [ @@ -384,6 +410,8 @@ export default class ChartWidget extends Widget { } dialog.show(); + //Set query report object so that it can be used while fetching filter values in the report + frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); dialog.set_values(this.filters); } @@ -391,6 +419,9 @@ export default class ChartWidget extends Widget { this.save_chart_config_for_user(null, 1); this.chart_settings = {}; this.filters = null; + this.selected_time_interval = null; + this.selected_timespan = null; + this.selected_heatmap_year = null; } save_chart_config_for_user(config, reset=0) { @@ -458,58 +489,25 @@ export default class ChartWidget extends Widget { time_interval: args && args.time_interval ? args.time_interval : null, timespan: args && args.timespan ? args.timespan : null, from_date: args && args.from_date ? args.from_date : null, - to_date: args && args.to_date ? args.to_date : null + to_date: args && args.to_date ? args.to_date : null, + heatmap_year: args && args.heatmap_year ? args.heatmap_year : null, }; } return frappe.xcall(method, args); } render() { - const chart_type_map = { - Line: "line", - Bar: "bar", - Percentage: "percentage", - Pie: "pie", - Donut: "donut" - }; - - let colors = []; - - if (this.chart_doc.y_axis.length) { - this.chart_doc.y_axis.map(field => { - colors.push(field.color); - }); - } else if (["Line", "Bar"].includes(this.chart_doc.type)) { - colors = [this.chart_doc.color || []]; - } - - if (!this.data || !this.data.labels.length || !Object.keys(this.data).length) { + if (!this.data || !this.data.labels || !Object.keys(this.data).length) { this.chart_wrapper.hide(); this.loading.hide(); - this.$summary.hide(); + this.$summary && this.$summary.hide(); this.empty.show(); } else { this.loading.hide(); this.empty.hide(); this.chart_wrapper.show(); - let chart_args = { - data: this.data, - type: chart_type_map[this.chart_doc.type], - colors: colors, - height: this.height, - axisOptions: { - xIsSeries: this.chart_doc.timeseries, - shortenYAxisNumbers: 1 - } - }; - - if (this.chart_doc.custom_options) { - let custom_options = JSON.parse(this.chart_doc.custom_options); - for (let key in custom_options) { - chart_args[key] = custom_options[key]; - } - } + const chart_args = this.get_chart_args(); if (!this.dashboard_chart) { this.dashboard_chart = new frappe.Chart( @@ -519,7 +517,93 @@ export default class ChartWidget extends Widget { } else { this.dashboard_chart.update(this.data); } + this.width == "Full" && this.summary && this.set_summary(); + this.chart_doc.type == 'Heatmap' && this.render_heatmap_legend(); + } + } + + get_chart_args() { + let colors = this.get_chart_colors(); + + const chart_type_map = { + Line: "line", + Bar: "bar", + Percentage: "percentage", + Pie: "pie", + Donut: "donut", + Heatmap: "heatmap" + }; + + let chart_args = { + data: this.data, + type: chart_type_map[this.chart_doc.type], + colors: colors, + height: this.height, + axisOptions: { + xIsSeries: this.chart_doc.timeseries, + shortenYAxisNumbers: 1 + } + }; + + if (this.chart_doc.type == "Heatmap") { + const heatmap_year = parseInt(this.selected_heatmap_year || this.chart_settings.heatmap_year || this.chart_doc.heatmap_year); + chart_args.data.start = new Date(`${heatmap_year}-01-01`); + chart_args.data.end = new Date(`${heatmap_year+1}-01-01`); + } + + let set_options = (options) => { + let custom_options = JSON.parse(options); + for (let key in custom_options) { + chart_args[key] = custom_options[key]; + } + }; + + if (this.custom_options) { + set_options(this.custom_options); + } + + if (this.chart_doc.custom_options) { + set_options(this.chart_doc.custom_options); + } + + return chart_args; + } + + get_chart_colors() { + let colors = []; + if (this.chart_doc.y_axis.length) { + this.chart_doc.y_axis.map(field => { + colors.push(field.color); + }); + } else if (["Line", "Bar"].includes(this.chart_doc.type)) { + colors = [this.chart_doc.color || "light-blue"]; + } else if (this.chart_doc.type == "Heatmap") { + colors = []; + } + + return colors; + } + + render_heatmap_legend() { + if (!this.$heatmap_legend && this.widget.width() > 991) { + this.$heatmap_legend = + $(` +
+
    +
  • +
  • +
  • +
  • +
  • +
+
+
${__("Less")}
+
${__("More")}
+
+
+ `); + this.body.append(this.$heatmap_legend); } } @@ -542,6 +626,10 @@ export default class ChartWidget extends Widget { let saved_filters = this.chart_settings.filters || null; this.filters = saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]"); + + if (this.chart_doc.type == 'Heatmap' && !this.chart_doc.heatmap_year) { + this.chart_doc.heatmap_year = frappe.dashboard_utils.get_year(frappe.datetime.now_date()); + } } get_settings() { diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index cda17e08bc..77cb8a59c2 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -119,7 +119,8 @@ export default class NumberCardWidget extends Widget { get_formatted_number() { const based_on_df = frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); - const shortened_number = shorten_number(this.number); + const default_country = frappe.sys_defaults.country; + const shortened_number = shorten_number(this.number, default_country); let number_parts = shortened_number.split(' '); const symbol = number_parts[1] || ''; diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index e347a70036..821824a2d2 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -25,7 +25,7 @@ export default class OnboardingWidget extends Widget { if (step.is_skipped) { status = "skipped"; - icon_class = "fa-times-circle-o"; + icon_class = "fa-check-circle-o"; } if (step.is_complete) { @@ -56,10 +56,17 @@ export default class OnboardingWidget extends Widget { // Setup actions let actions = { "Watch Video": () => this.show_video(step), - "Create Entry": () => this.show_quick_entry(step), + "Create Entry": () => { + if (step.show_full_form) { + this.create_entry(step); + } else { + this.show_quick_entry(step); + } + }, "Show Form Tour": () => this.show_form_tour(step), "Update Settings": () => this.update_settings(step), "View Report": () => this.open_report(step), + "Go to Page": () => this.go_to_page(step), }; $step.find("#title").on("click", actions[step.action]); @@ -68,6 +75,24 @@ export default class OnboardingWidget extends Widget { return $step; } + go_to_page(step) { + frappe.set_route(step.path).then(() => { + if (step.callback_message) { + let msg_dialog = frappe.msgprint({ + message: __(step.callback_message), + title: __(step.callback_title), + primary_action: { + action: () => { + msg_dialog.hide(); + }, + label: () => __("Continue"), + }, + wide: true, + }); + } + }); + } + open_report(step) { let route = generate_route({ name: step.reference_report, @@ -75,10 +100,9 @@ export default class OnboardingWidget extends Widget { is_query_report: ["Query Report", "Script Report"].includes( step.report_type ), - doctype: step.report_reference_doctype + doctype: step.report_reference_doctype, }); - let current_route = frappe.get_route(); frappe.set_route(route).then(() => { @@ -133,7 +157,7 @@ export default class OnboardingWidget extends Widget { msg_dialog.hide(); }, label: () => __("Continue"), - } + }, }); }); }; @@ -147,6 +171,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { frm.scroll_to_field(step.field); + frm.doc.__unsaved = true; }; frappe.route_hooks.after_save = (frm) => { @@ -204,6 +229,44 @@ export default class OnboardingWidget extends Widget { frappe.set_route("Form", step.reference_document); } + create_entry(step) { + let current_route = frappe.get_route(); + + frappe.route_hooks = {}; + let callback = () => { + frappe.msgprint({ + message: __("You're doing great, let's take you back to the onboarding page."), + title: __("Good Work 🎉"), + primary_action: { + action: () => { + frappe.set_route(current_route).then(() => { + this.mark_complete(step); + }); + }, + label: __("Continue"), + }, + }); + + frappe.msg_dialog.custom_onhide = () => { + this.mark_complete(step); + }; + }; + + if (step.is_submittable) { + frappe.route_hooks.after_save = () => { + frappe.msgprint({ + message: __("Submit this document to complete this step."), + title: __("Great") + }); + }; + frappe.route_hooks.after_submit = callback; + } else { + frappe.route_hooks.after_save = callback; + } + + frappe.set_route(`Form/${step.reference_document}/New ${step.reference_document} 1`); + } + show_quick_entry(step) { let current_route = frappe.get_route_str(); frappe.ui.form.make_quick_entry( @@ -221,7 +284,7 @@ export default class OnboardingWidget extends Widget { }); }, label: __("Continue"), - } + }, }); frappe.msg_dialog.custom_onhide = () => { @@ -271,8 +334,10 @@ export default class OnboardingWidget extends Widget { update_step_status(step, status, value, callback) { let icon_class = { is_complete: "fa-check-circle-o", - is_skipped: "fa-times-circle-o", + is_skipped: "fa-check-circle-o", }; + // Clear any hooks + frappe.route_hooks = {}; frappe .call("frappe.desk.desktop.update_onboarding_step", { @@ -394,4 +459,4 @@ export default class OnboardingWidget extends Widget { }); dismiss.appendTo(this.action_area); } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index 0d93bb3784..c92bdc1b5f 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -24,6 +24,8 @@ function generate_route(item) { route = "List/" + item.doctype + "/Report/" + item.name; } else if (type === "page") { route = item.name; + } else if (type === "dashboard") { + route = "dashboard/" + item.name; } route = "#" + route; @@ -125,19 +127,44 @@ function go_to_list_with_filters(doctype, filters) { }); } -function shorten_number(number) { +function shorten_number(number, country) { + country = country || ''; + const number_system = get_number_system(country); let x = Math.abs(Math.round(number)); - - switch (true) { - case x >= 1.0e+12: - return Math.round(number/1.0e+12) + " T"; - case x >= 1.0e+9: - return Math.round(number/1.0e+9) + " B"; - case x >= 1.0e+6: - return Math.round(number/1.0e+6) + " M"; - default: - return number.toFixed(); + for (const map of number_system) { + if (x >= map.divisor) { + return Math.round(number/map.divisor) + ' ' + map.symbol; + } } + return number.toFixed(); +} + +function get_number_system(country) { + let number_system_map = { + 'India': + [{ + divisor: 1.0e+7, + symbol: 'Cr' + }, + { + divisor: 1.0e+5, + symbol: 'Lakh' + }], + '': + [{ + divisor: 1.0e+12, + symbol: 'T' + }, + { + divisor: 1.0e+9, + symbol: 'B' + }, + { + divisor: 1.0e+6, + symbol: 'M' + }] + }; + return number_system_map[country]; } export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number }; \ No newline at end of file diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 31215a40c3..5c44533b37 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -145,7 +145,7 @@ class ShortcutDialog extends WidgetDialog { fieldname: "type", label: "Type", reqd: 1, - options: "DocType\nReport\nPage", + options: "DocType\nReport\nPage\nDashboard", onchange: () => { if (this.dialog.get_value("type") == "DocType") { this.dialog.fields_dict.link_to.get_query = () => { diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index 8c8dd02968..e82cbc6edf 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -52,6 +52,7 @@ export default class WidgetGroup {
`); this.widget_area = widget_area; + if (this.hidden) this.widget_area.hide(); this.title_area = widget_area.find(".widget-group-title"); this.control_area = widget_area.find(".widget-group-control"); this.body = widget_area.find(".widget-group-body"); @@ -96,7 +97,7 @@ export default class WidgetGroup { } customize() { - this.widget_area.show(); + if (!this.hidden) this.widget_area.show(); this.widgets_list.forEach((wid) => { wid.customize(this.options); }); diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less index 1e64533079..eef0b29875 100644 --- a/frappe/public/less/desktop.less +++ b/frappe/public/less/desktop.less @@ -293,6 +293,75 @@ } } + &.dashboard-widget-box.heatmap-chart { + min-height: 0px; + height: 180px; + + .widget-footer { + display: none; + } + + .widget-control { + z-index: 1; + } + + .frappe-chart .chart-legend { + display: none; + } + + .chart-loading-state { + height: 160px !important; + } + + .widget-body { + display: flex; + max-height: 100%; + margin: auto; + margin-top: -15px; + + .chart-container { + height: 100%; + .frappe-chart { + height: 100%; + } + } + + .heatmap-legend { + display: flex; + margin: 45px 20px 0 20px; + + .legend-colors { + padding-left: 1; + padding-left: 15px; + list-style: none; + } + + li { + width: 10px; + height: 10px; + margin: 5px; + } + + .legend-label { + color: #555b51; + font-size: 11px; + margin-left: 15px; + line-height: 1.6em; + } + + @media (max-width: 991px) { + display: none; + } + } + } + } + + @media (max-width: 768px) { + &.dashboard-widget-box.heatmap-chart { + display: none; + } + } + &.onboarding-widget-box { margin-bottom: 50px; margin-top: 10px; diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index 8e43b05122..df0334c14f 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -314,11 +314,20 @@ h6.uppercase, .h6.uppercase { } } -.form-section:not(:last-child), +.hide-border { + border-top: none !important; + padding-top: 0px; +} + +.form-section:not(:first-child) { + border-top: 1px solid @border-color; +} + .form-inner-toolbar { border-bottom: 1px solid @border-color; } + .empty-section { display: none !important; } diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index a4c2c4bb70..0a04db2c3e 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -103,7 +103,8 @@ class BackupGenerator: cmd_string = """tar -cf %s %s""" % (backup_path, files_path) err, out = frappe.utils.execute_in_shell(cmd_string) - print('Backed up files', os.path.abspath(backup_path)) + if verbose: + print('Backed up files', os.path.abspath(backup_path)) def take_dump(self): import frappe.utils @@ -151,7 +152,6 @@ def get_backup(): This function is executed when the user clicks on Toos > Download Backup """ - #if verbose: print frappe.db.cur_db_name + " " + conf.db_password delete_temp_backups() odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\ frappe.conf.db_password, db_host = frappe.db.host) diff --git a/frappe/utils/commands.py b/frappe/utils/commands.py new file mode 100644 index 0000000000..99322b50ba --- /dev/null +++ b/frappe/utils/commands.py @@ -0,0 +1,42 @@ +import functools +import requests +from terminaltables import AsciiTable + + +@functools.lru_cache(maxsize=1024) +def get_first_party_apps(): + """Get list of all apps under orgs: frappe. erpnext from GitHub""" + apps = [] + for org in ["frappe", "erpnext"]: + req = requests.get(f"https://api.github.com/users/{org}/repos", {"type": "sources", "per_page": 200}) + if req.ok: + apps.extend([x["name"] for x in req.json()]) + return apps + + +def render_table(data): + print(AsciiTable(data).table) + + +def add_line_after(function): + """Adds an extra line to STDOUT after the execution of a function this decorates""" + def empty_line(*args, **kwargs): + result = function(*args, **kwargs) + print() + return result + return empty_line + + +def log(message, colour=''): + """Coloured log outputs to STDOUT""" + colours = { + "nc": '\033[0m', + "blue": '\033[94m', + "green": '\033[92m', + "yellow": '\033[93m', + "red": '\033[91m', + "silver": '\033[90m' + } + colour = colours.get(colour, "") + end_line = '\033[0m' + print(colour + message + end_line) diff --git a/frappe/utils/connections.py b/frappe/utils/connections.py new file mode 100644 index 0000000000..6bd24d57ec --- /dev/null +++ b/frappe/utils/connections.py @@ -0,0 +1,44 @@ +import socket + +from six.moves.urllib.parse import urlparse +from frappe import get_conf + +config = get_conf() +REDIS_KEYS = ('redis_cache', 'redis_queue', 'redis_socketio') + + +def is_open(ip, port, timeout=10): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + try: + s.connect((ip, int(port))) + s.shutdown(socket.SHUT_RDWR) + return True + except socket.error: + return False + finally: + s.close() + + +def check_database(): + db_type = config.get("db_type", "mariadb") + db_host = config.get("db_host", "localhost") + db_port = config.get("db_port", 3306 if db_type == "mariadb" else 5342) + return {db_type: is_open(db_host, db_port)} + + +def check_redis(redis_services=None): + services = redis_services or REDIS_KEYS + status = {} + for conn in services: + redis_url = urlparse(config.get(conn)).netloc + redis_host, redis_port = redis_url.split(":") + status[conn] = is_open(redis_host, redis_port) + return status + + +def check_connection(redis_services=None): + service_status = {} + service_status.update(check_database()) + service_status.update(check_redis(redis_services)) + return service_status diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py index bb835d6561..f06f9272b8 100644 --- a/frappe/utils/dashboard.py +++ b/frappe/utils/dashboard.py @@ -41,6 +41,7 @@ def generate_and_cache_results(args, function, cache_key, chart): to_date = args.to_date or None, time_interval = args.time_interval or None, timespan = args.timespan or None, + heatmap_year = args.heatmap_year or None ) except TypeError as e: if str(e) == "'NoneType' object is not iterable": diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 58c74a905d..9796aa3c4a 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -213,6 +213,19 @@ def get_datetime_str(datetime_obj): datetime_obj = get_datetime(datetime_obj) return datetime_obj.strftime(DATETIME_FORMAT) +def get_date_str(date_obj): + if isinstance(date_obj, string_types): + date_obj = get_datetime(date_obj) + return date_obj.strftime(DATE_FORMAT) + +def get_time_str(timedelta_obj): + if isinstance(timedelta_obj, string_types): + timedelta_obj = to_timedelta(timedelta_obj) + + hours, remainder = divmod(timedelta_obj.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return "{0}:{1}:{2}".format(hours, minutes, seconds) + def get_user_date_format(): """Get the current user date format. The result will be cached.""" if getattr(frappe.local, "user_date_format", None) is None: diff --git a/frappe/website/render.py b/frappe/website/render.py index 2e448e7d5b..c1bca3f5c5 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -7,8 +7,10 @@ from frappe import _ import frappe.sessions from frappe.utils import cstr import os, mimetypes, json +import re import six +from bs4 import BeautifulSoup from six import iteritems from werkzeug.wrappers import Response from werkzeug.routing import Map, Rule, NotFound @@ -128,12 +130,35 @@ def build_response(path, data, http_status_code, headers=None): response.headers["X-Page-Name"] = path.encode("ascii", errors="xmlcharrefreplace") response.headers["X-From-Cache"] = frappe.local.response.from_cache or False + add_preload_headers(response) if headers: for key, val in iteritems(headers): response.headers[key] = val.encode("ascii", errors="xmlcharrefreplace") return response + +def add_preload_headers(response): + try: + preload = [] + soup = BeautifulSoup(response.data, "lxml") + for elem in soup.find_all('script', src=re.compile(".*")): + preload.append(("script", elem.get("src"))) + + for elem in soup.find_all('link', rel="stylesheet"): + preload.append(("style", elem.get("href"))) + + links = [] + for type, link in preload: + links.append("; rel=preload; as={}".format(link.lstrip("/"), type)) + + if links: + response.headers["Link"] = ",".join(links) + except Exception: + import traceback + traceback.print_exc() + + def render_page_by_language(path): translated_languages = frappe.get_hooks("translated_languages_for_website") user_lang = guess_language(translated_languages) diff --git a/frappe/website/website_theme/standard/standard.json b/frappe/website/website_theme/standard/standard.json index 6135d51a8e..9365d5be27 100644 --- a/frappe/website/website_theme/standard/standard.json +++ b/frappe/website/website_theme/standard/standard.json @@ -18,4 +18,4 @@ "theme": "Standard", "theme_scss": "$enable-shadows: false;\n$enable-gradients: false;\n$enable-rounded: true;\n\n// Bootstrap Variable Overrides\n\n\n@import \"frappe/public/scss/website\";\n\n\n\n// Custom Theme\n", "theme_url": "/assets/css/standard_style.css" -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index f05e0f3870..431f216afa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,6 +59,7 @@ semantic-version==2.8.4 six==1.14.0 sqlparse==0.2.4 stripe==2.40.0 +terminaltables==3.1.0 unittest-xml-reporting==2.5.2 urllib3==1.25.8 watchdog==0.8.0