From 0f8e164e8e1f3a6ca9a8c119822d5cadb106326d Mon Sep 17 00:00:00 2001 From: David Angulo Date: Thu, 25 Mar 2021 19:38:56 -0600 Subject: [PATCH 01/58] feat: Add 'Automatic' option for desk theme --- frappe/core/doctype/user/user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 747ace5de6..10e9e63d0e 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -594,7 +594,7 @@ "fieldname": "desk_theme", "fieldtype": "Select", "label": "Desk Theme", - "options": "Light\nDark" + "options": "Light\nDark\nAutomatic" }, { "fieldname": "module_profile", From bfce0ca52f4d6695955c4ae950b2ab1d50fde5c1 Mon Sep 17 00:00:00 2001 From: David Angulo Date: Thu, 25 Mar 2021 19:39:15 -0600 Subject: [PATCH 02/58] feat: Add desk_theme to bootinfo --- frappe/sessions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/sessions.py b/frappe/sessions.py index 3babf1db12..bb54418a17 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -159,6 +159,8 @@ def get(): bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete')) bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup')) + bootinfo['desk_theme'] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or 'Light' + return bootinfo def get_csrf_token(): From 72126a8645e0112c4ce5925d4e54efcc534483fe Mon Sep 17 00:00:00 2001 From: David Angulo Date: Thu, 25 Mar 2021 19:41:19 -0600 Subject: [PATCH 03/58] feat: Change default desk theme behaviour --- frappe/www/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/www/app.py b/frappe/www/app.py index 6088c413dc..656ebbe45c 100644 --- a/frappe/www/app.py +++ b/frappe/www/app.py @@ -46,7 +46,7 @@ def get_context(context): "include_css": hooks["app_include_css"], "sounds": hooks["sounds"], "boot": boot if context.get("for_mobile") else boot_json, - "desk_theme": desk_theme or "Light", + "desk_theme": desk_theme if not desk_theme == 'Automatic' else 'Light', "csrf_token": csrf_token, "google_analytics_id": frappe.conf.get("google_analytics_id"), "google_analytics_anonymize_ip": frappe.conf.get("google_analytics_anonymize_ip"), From 1100180576a3560774f6b7f210479e87ae43297d Mon Sep 17 00:00:00 2001 From: David Angulo Date: Thu, 25 Mar 2021 19:43:51 -0600 Subject: [PATCH 04/58] feat: Add listener to system theme change, to update desk theme accordingly --- frappe/public/js/frappe/desk.js | 6 +++++ frappe/public/js/frappe/ui/theme_switcher.js | 26 +++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 250d308b7e..8094c0261b 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -63,6 +63,12 @@ frappe.Application = Class.extend({ } }); + if(frappe.boot.desk_theme == 'Automatic') { + frappe.ui.add_system_theme_switch_listener(); + const startup_theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light'; + frappe.ui.toggle_theme(startup_theme); + } + this.set_rtl(); // page container diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 317198bca5..d78dd6455c 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -82,11 +82,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { return preview; } - toggle_theme(theme) { + toggle_theme(theme, save_preferences=true) { this.current_theme = theme.toLowerCase(); document.documentElement.setAttribute("data-theme", this.current_theme); frappe.show_alert("Theme Changed", 3); + if(!save_preferences) { + return; + } + frappe.xcall("frappe.core.doctype.user.user.switch_theme", { theme: toTitle(theme) }); @@ -99,3 +103,23 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { this.dialog.hide(); } }; + +frappe.ui.add_system_theme_switch_listener = function() { + const toggle_theme = frappe.ui.toggle_theme; + + frappe.ui.dark_theme_media_query.addEventListener('change', function(e) { + if (e.matches) { + toggle_theme('dark'); + return; + } + + toggle_theme('light'); + }) +} + +frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)"); + +frappe.ui.toggle_theme = function(theme) { + const theme_switcher = new frappe.ui.ThemeSwitcher(); + theme_switcher.toggle_theme(theme, false); +} \ No newline at end of file From c1fd70b997484797a6f3d464ab4ab807c08fc935 Mon Sep 17 00:00:00 2001 From: David Angulo Date: Thu, 25 Mar 2021 20:51:58 -0600 Subject: [PATCH 05/58] feat: Add options to ThemeSwitcher.toggle_theme to configure if we want to show an alert and save preferences on change --- frappe/public/js/frappe/ui/theme_switcher.js | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index d78dd6455c..fb43c1fae0 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -82,18 +82,19 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { return preview; } - toggle_theme(theme, save_preferences=true) { + toggle_theme(theme, options = {save_preferences:true, show_alert:true}) { this.current_theme = theme.toLowerCase(); document.documentElement.setAttribute("data-theme", this.current_theme); - frappe.show_alert("Theme Changed", 3); - if(!save_preferences) { - return; + if (options && options.show_alert) { + frappe.show_alert("Theme Changed", 3); + } + + if(options && options.save_preferences) { + frappe.xcall("frappe.core.doctype.user.user.switch_theme", { + theme: toTitle(theme) + }); } - - frappe.xcall("frappe.core.doctype.user.user.switch_theme", { - theme: toTitle(theme) - }); } show() { this.dialog.show(); @@ -121,5 +122,8 @@ frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dar frappe.ui.toggle_theme = function(theme) { const theme_switcher = new frappe.ui.ThemeSwitcher(); - theme_switcher.toggle_theme(theme, false); + theme_switcher.toggle_theme(theme, { + save_preferences: false, + show_alert: false + }); } \ No newline at end of file From a1579db65dde95520705100176dd7ddff637e5a9 Mon Sep 17 00:00:00 2001 From: David Angulo Date: Thu, 25 Mar 2021 21:08:05 -0600 Subject: [PATCH 06/58] fix: fix sider validation --- frappe/public/js/frappe/desk.js | 2 +- frappe/public/js/frappe/ui/theme_switcher.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 8094c0261b..682f730d3c 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -63,7 +63,7 @@ frappe.Application = Class.extend({ } }); - if(frappe.boot.desk_theme == 'Automatic') { + if (frappe.boot.desk_theme == 'Automatic') { frappe.ui.add_system_theme_switch_listener(); const startup_theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light'; frappe.ui.toggle_theme(startup_theme); diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index fb43c1fae0..56263824dd 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -82,7 +82,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { return preview; } - toggle_theme(theme, options = {save_preferences:true, show_alert:true}) { + toggle_theme(theme, options = { save_preferences:true, show_alert:true }) { this.current_theme = theme.toLowerCase(); document.documentElement.setAttribute("data-theme", this.current_theme); @@ -90,7 +90,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { frappe.show_alert("Theme Changed", 3); } - if(options && options.save_preferences) { + if (options && options.save_preferences) { frappe.xcall("frappe.core.doctype.user.user.switch_theme", { theme: toTitle(theme) }); @@ -115,8 +115,8 @@ frappe.ui.add_system_theme_switch_listener = function() { } toggle_theme('light'); - }) -} + }); +}; frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)"); @@ -126,4 +126,4 @@ frappe.ui.toggle_theme = function(theme) { save_preferences: false, show_alert: false }); -} \ No newline at end of file +}; \ No newline at end of file From 2f32c4f19698c1031fa2fa83dcd3fd8b19051b4f Mon Sep 17 00:00:00 2001 From: David Angulo Date: Fri, 26 Mar 2021 10:01:18 -0600 Subject: [PATCH 07/58] fix: fix sider validation --- frappe/public/js/frappe/ui/theme_switcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 56263824dd..fbf6575bc4 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -82,7 +82,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { return preview; } - toggle_theme(theme, options = { save_preferences:true, show_alert:true }) { + toggle_theme(theme, options = { save_preferences: true, show_alert: true }) { this.current_theme = theme.toLowerCase(); document.documentElement.setAttribute("data-theme", this.current_theme); From e00eb958367fc7567cf1a51f0370e64a57b8f95c Mon Sep 17 00:00:00 2001 From: David Angulo Date: Tue, 6 Apr 2021 10:35:56 -0500 Subject: [PATCH 08/58] fix: Update modified timestamp so the update will pickup the changes --- frappe/core/doctype/user/user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 10e9e63d0e..4d8d907ee6 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -669,7 +669,7 @@ } ], "max_attachments": 5, - "modified": "2021-02-01 16:11:06.037543", + "modified": "2021-04-06 16:11:06.037543", "modified_by": "Administrator", "module": "Core", "name": "User", From d328b65122aecdd1490251b671f5dc203539b848 Mon Sep 17 00:00:00 2001 From: mhbu50 Date: Sat, 24 Jul 2021 21:37:15 +0300 Subject: [PATCH 09/58] fix: Translate Strings --- frappe/templates/print_formats/standard_macros.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index f8dc6c370c..a6238b65d2 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -192,9 +192,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% else %} From 64c18a31870665b1d4e04b1b257a8bfe3db2f675 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 26 Oct 2021 16:06:32 +0530 Subject: [PATCH 10/58] feat: allow more print page size options --- frappe/__init__.py | 11 +- .../print_settings/print_settings.json | 16 ++- .../doctype/print_settings/print_settings.py | 15 ++- .../public/js/frappe/list/bulk_operations.js | 119 +++++++++++------- frappe/public/js/frappe/model/meta.js | 9 ++ frappe/utils/pdf.py | 19 ++- frappe/utils/print_format.py | 9 +- 7 files changed, 140 insertions(+), 58 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index c8245b0bf0..3d3acd99e2 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1512,8 +1512,8 @@ def format(*args, **kwargs): import frappe.utils.formatters return frappe.utils.formatters.format_value(*args, **kwargs) -def get_print(doctype=None, name=None, print_format=None, style=None, - html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None): +def get_print(doctype=None, name=None, print_format=None, style=None, html=None, + as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None): """Get Print Format for given document. :param doctype: DocType of document. @@ -1532,15 +1532,16 @@ def get_print(doctype=None, name=None, print_format=None, style=None, local.form_dict.doc = doc local.form_dict.no_letterhead = no_letterhead - options = None + if not pdf_options: + pdf_options = {} if password: - options = {'password': password} + pdf_options['password'] = password if not html: html = get_response_content("printview") if as_pdf: - return get_pdf(html, output = output, options = options) + return get_pdf(html, options=pdf_options, output=output) else: return html diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index babbae248d..f45de7637d 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -10,6 +10,8 @@ "repeat_header_footer", "column_break_4", "pdf_page_size", + "pdf_page_height", + "pdf_page_width", "view_link_in_email", "with_letterhead", "allow_print_for_draft", @@ -56,7 +58,7 @@ "fieldname": "pdf_page_size", "fieldtype": "Select", "label": "PDF Page Size", - "options": "A4\nLetter" + "options": "A0\nA1\nA2\nA3\nA4\nA5\nA6\nA7\nA8\nA9\nB0\nB1\nB2\nB3\nB4\nB5\nB6\nB7\nB8\nB9\nB10\nC5E\nComm10E\nDLE\nExecutive\nFolio\nLedger\nLegal\nLetter\nTabloid\nCustom" }, { "fieldname": "view_link_in_email", @@ -156,6 +158,18 @@ "fieldname": "font_size", "fieldtype": "Float", "label": "Font Size" + }, + { + "depends_on": "eval:doc.pdf_page_size == \"Custom\"", + "fieldname": "pdf_page_height", + "fieldtype": "Float", + "label": "PDF Page Height (in mm)" + }, + { + "depends_on": "eval:doc.pdf_page_size == \"Custom\"", + "fieldname": "pdf_page_width", + "fieldtype": "Float", + "label": "PDF Page Width (in mm)" } ], "icon": "fa fa-cog", diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index ff00317cf8..5eb8d8b215 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -8,14 +8,23 @@ from frappe.utils import cint from frappe.model.document import Document + class PrintSettings(Document): + def validate(self): + if self.pdf_page_size == "Custom" and ( + not self.pdf_page_height or not self.pdf_page_width + ): + frappe.throw(_("Page height and width cannot be zero")) + def on_update(self): frappe.clear_cache() + @frappe.whitelist() def is_print_server_enabled(): - if not hasattr(frappe.local, 'enable_print_server'): - frappe.local.enable_print_server = cint(frappe.db.get_single_value('Print Settings', - 'enable_print_server')) + if not hasattr(frappe.local, "enable_print_server"): + frappe.local.enable_print_server = cint( + frappe.db.get_single_value("Print Settings", "enable_print_server") + ) return frappe.local.enable_print_server diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index ee6e6d753c..94ec9d4e67 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -24,51 +24,84 @@ export default class BulkOperations { return; } - if (valid_docs.length > 0) { - const dialog = new frappe.ui.Dialog({ - title: __('Print Documents'), - fields: [ - { - 'fieldtype': 'Select', - 'label': __('Letter Head'), - 'fieldname': 'letter_sel', - 'default': __('No Letterhead'), - options: this.get_letterhead_options() - }, - { - 'fieldtype': 'Select', - 'label': __('Print Format'), - 'fieldname': 'print_sel', - options: frappe.meta.get_print_formats(this.doctype) - } - ] - }); - - dialog.set_primary_action(__('Print'), args => { - if (!args) return; - const default_print_format = frappe.get_meta(this.doctype).default_print_format; - const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1; - const print_format = args.print_sel ? args.print_sel : default_print_format; - const json_string = JSON.stringify(valid_docs); - const letterhead = args.letter_sel; - const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' + - 'doctype=' + encodeURIComponent(this.doctype) + - '&name=' + encodeURIComponent(json_string) + - '&format=' + encodeURIComponent(print_format) + - '&no_letterhead=' + (with_letterhead ? '0' : '1') + - '&letterhead=' + encodeURIComponent(letterhead) - ); - - if (!w) { - frappe.msgprint(__('Please enable pop-ups')); - return; - } - }); - - dialog.show(); - } else { + if (valid_docs.length === 0) { frappe.msgprint(__('Select atleast 1 record for printing')); + return; } + + const dialog = new frappe.ui.Dialog({ + title: __('Print Documents'), + fields: [{ + fieldtype: 'Select', + label: __('Letter Head'), + fieldname: 'letter_sel', + default: __('No Letterhead'), + options: this.get_letterhead_options() + }, + { + fieldtype: 'Select', + label: __('Print Format'), + fieldname: 'print_sel', + options: frappe.meta.get_print_formats(this.doctype) + }, + { + fieldtype: 'Select', + label: __('Page Size'), + fieldname: 'page_size', + options: frappe.meta.get_print_sizes(), + default: print_settings.pdf_page_size + }, + { + fieldtype: 'Float', + label: __('Page Height (in mm)'), + fieldname: 'page_height', + depends_on: 'eval:doc.page_size == "Custom"', + default: print_settings.pdf_page_height + }, + { + fieldtype: 'Float', + label: __('Page Width (in mm)'), + fieldname: 'page_width', + depends_on: 'eval:doc.page_size == "Custom"', + default: print_settings.pdf_page_width + }] + }); + + dialog.set_primary_action(__('Print'), args => { + if (!args) return; + const default_print_format = frappe.get_meta(this.doctype).default_print_format; + const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1; + const print_format = args.print_sel ? args.print_sel : default_print_format; + const json_string = JSON.stringify(valid_docs); + const letterhead = args.letter_sel; + + let pdf_options; + if (args.page_size === "Custom") { + if (args.page_height === 0 || args.page_width === 0) { + frappe.throw(__('Page height and width cannot be zero')); + } + pdf_options = JSON.stringify({ "page-height": args.page_height, "page-width": args.page_width }); + } else { + pdf_options = JSON.stringify({ "page-size": args.page_size }); + } + + const w = window.open( + '/api/method/frappe.utils.print_format.download_multi_pdf?' + + 'doctype=' + encodeURIComponent(this.doctype) + + '&name=' + encodeURIComponent(json_string) + + '&format=' + encodeURIComponent(print_format) + + '&no_letterhead=' + (with_letterhead ? '0' : '1') + + '&letterhead=' + encodeURIComponent(letterhead) + + '&options=' + encodeURIComponent(pdf_options) + ); + + if (!w) { + frappe.msgprint(__('Please enable pop-ups')); + return; + } + }); + + dialog.show(); } get_letterhead_options () { diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index 6ee9084adc..3c9ddc4d96 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -192,6 +192,15 @@ $.extend(frappe.meta, { } }, + get_print_sizes: function() { + return [ + "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", + "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10", + "C5E", "Comm10E", "DLE", "Executive", "Folio", "Ledger", "Legal", + "Letter", "Tabloid", "Custom" + ]; + }, + get_print_formats: function(doctype) { var print_format_list = ["Standard"]; var default_print_format = locals.DocType[doctype].default_print_format; diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index 90bb4f63de..9a7c0889b5 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -95,7 +95,7 @@ def prepare_options(html, options): 'quiet': None, # 'no-outline': None, 'encoding': "UTF-8", - #'load-error-handling': 'ignore' + # 'load-error-handling': 'ignore' }) if not options.get("margin-right"): @@ -111,8 +111,21 @@ def prepare_options(html, options): options.update(get_cookie_options()) # page size - if not options.get("page-size"): - options['page-size'] = frappe.db.get_single_value("Print Settings", "pdf_page_size") or "A4" + pdf_page_size = ( + options.get("page-size") + or frappe.db.get_single_value("Print Settings", "pdf_page_size") + or "A4" + ) + + if pdf_page_size == "Custom": + options["page-height"] = options.get("page-height") or frappe.db.get_single_value( + "Print Settings", "pdf_page_height" + ) + options["page-width"] = options.get("page-width") or frappe.db.get_single_value( + "Print Settings", "pdf_page_width" + ) + else: + options["page-size"] = pdf_page_size return html, options diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 6dfa3a350b..06f15ced27 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -11,7 +11,7 @@ base_template_path = "www/printview.html" standard_format = "templates/print_formats/standard.html" @frappe.whitelist() -def download_multi_pdf(doctype, name, format=None, no_letterhead=0): +def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None): """ Concatenate multiple docs as PDF . @@ -54,18 +54,21 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=0): import json output = PdfFileWriter() + if isinstance(options, str): + options = json.loads(options) + if not isinstance(doctype, dict): result = json.loads(name) # Concatenating pdf files for i, ss in enumerate(result): - output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output, no_letterhead=no_letterhead) + output = frappe.get_print(doctype, ss, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options) frappe.local.response.filename = "{doctype}.pdf".format(doctype=doctype.replace(" ", "-").replace("/", "-")) else: for doctype_name in doctype: for doc_name in doctype[doctype_name]: try: - output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output, no_letterhead=no_letterhead) + output = frappe.get_print(doctype_name, doc_name, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options) except Exception: frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name)) frappe.local.response.filename = "{}.pdf".format(name) From c5464e2c5a0f72039a6ace5df17e383e8e7b5c10 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 4 Nov 2021 14:50:37 +0530 Subject: [PATCH 11/58] fix: requested changes --- frappe/__init__.py | 3 +-- frappe/printing/doctype/print_settings/print_settings.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index cf5642b452..63ff2f83f7 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1542,8 +1542,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, html=None, local.form_dict.doc = doc local.form_dict.no_letterhead = no_letterhead - if not pdf_options: - pdf_options = {} + pdf_options = pdf_options or {} if password: pdf_options['password'] = password diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index 5eb8d8b215..3253cea2dc 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -11,8 +11,8 @@ from frappe.model.document import Document class PrintSettings(Document): def validate(self): - if self.pdf_page_size == "Custom" and ( - not self.pdf_page_height or not self.pdf_page_width + if self.pdf_page_size == "Custom" and not ( + self.pdf_page_height and self.pdf_page_width ): frappe.throw(_("Page height and width cannot be zero")) From 952921ef795ac44c8164b1f2ad0b02c160d9b678 Mon Sep 17 00:00:00 2001 From: Aradhya-Tripathi Date: Fri, 12 Nov 2021 14:32:24 +0530 Subject: [PATCH 12/58] fix: fixed list filters in get_values --- frappe/database/database.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index a7dd9b6b66..49187f9eaa 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -568,11 +568,10 @@ class Database(object): def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True): names = list(filter(None, names)) - if names: return self.get_all(doctype, - fields=['name', field], - filters=[['name', 'in', names]], + fields=field, + filters=names, debug=debug, as_list=1, run=run) else: return {} From 41f285b13e47ee533dc31069931581f45cff87ad Mon Sep 17 00:00:00 2001 From: Aradhya-Tripathi Date: Sun, 14 Nov 2021 21:46:06 +0530 Subject: [PATCH 13/58] feat: Added test cases for get values --- frappe/tests/test_db.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 9077655dc9..6e49ef3c54 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -34,7 +34,21 @@ class TestDB(unittest.TestCase): self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0], frappe.db.get_value("User", {"name": [">=", "t"]})) - self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower()) + self.assertIn( + "concat_ws", + frappe.db.get_value( + "User", + filters={"name": "Administrator"}, + fieldname=Concat_ws(" ", "LastName"), + run=False, + ).lower(), + ) + self.assertEqual( + frappe.db.sql("select email from tabUser where name='Administrator' order by modified DESC"), + frappe.db.get_values( + "User", filters=[["name", "=", "Administrator"]], fieldname="email" + ), + ) def test_set_value(self): todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert() From 2e28ee5b25ad407d178b19b7efd1a2943603f073 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 15 Nov 2021 12:10:50 +0530 Subject: [PATCH 14/58] fix: Update Installed Applications on remove_app Removing an app from a site should sync the Installed Applications doctype --- frappe/installer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/installer.py b/frappe/installer.py index d1a13fdaab..9eed44ea15 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -240,6 +240,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) if not dry_run: remove_from_installed_apps(app_name) + frappe.get_single('Installed Applications').update_versions() frappe.db.commit() click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green") From ef39843f6ba8b24912fdb41a62df186dbb39bd15 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 15 Nov 2021 13:32:34 +0530 Subject: [PATCH 15/58] fix(cli): Add new line after each site's migrate --- frappe/commands/site.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index c5f78e2680..3c7f2f5525 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -461,6 +461,7 @@ def migrate(context, skip_failing=False, skip_search_index=False): skip_search_index=skip_search_index ) finally: + print() frappe.destroy() if not context.sites: raise SiteNotSpecifiedError From 7f43e102a4f866138c7683039faea31570bde28f Mon Sep 17 00:00:00 2001 From: Summayya Date: Tue, 16 Nov 2021 09:56:19 +0530 Subject: [PATCH 16/58] fix: pparent doctype freeze --- frappe/public/js/frappe/views/container.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/frappe/views/container.js b/frappe/public/js/frappe/views/container.js index 126feea16e..cf1d6c9466 100644 --- a/frappe/public/js/frappe/views/container.js +++ b/frappe/public/js/frappe/views/container.js @@ -42,7 +42,6 @@ frappe.views.Container = class Container { cur_page = this; if(this.page && this.page.label === label) { $(this.page).trigger('show'); - return; } var me = this; From 0b9a5a4c46accc29914af93acf14927ccfed41ee Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Tue, 16 Nov 2021 15:27:46 +0530 Subject: [PATCH 17/58] feat: add for query report --- .../public/js/frappe/views/reports/query_report.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 04cc1b9880..f1e2408eb5 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1789,6 +1789,19 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.$chart.toggle(flag); this.$summary.toggle(flag); } + + get_checked_items(only_docnames) { + const indexes = this.datatable.rowmanager.getCheckedRows(); + + return indexes.reduce((items, i) => { + if (i === undefined) return items; + + const item = this.data[i]; + items.push(only_docnames ? item.name : item); + return items; + }, []); + } + // backward compatibility get get_values() { return this.get_filter_values; From a6cd8e0d8e8d66f231aae8f6bde90baa5fc3ec23 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 16 Nov 2021 16:45:22 +0530 Subject: [PATCH 18/58] fix: Added checkbox to enable disable email notification for blog comment and feedback --- .../templates/includes/comments/comments.py | 29 +++++++------------ .../templates/includes/feedback/feedback.py | 6 ++-- .../website/doctype/blog_post/blog_post.json | 9 +++++- frappe/website/doctype/blog_post/blog_post.py | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/frappe/templates/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py index e352c7368b..99afb580d8 100644 --- a/frappe/templates/includes/comments/comments.py +++ b/frappe/templates/includes/comments/comments.py @@ -28,16 +28,6 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference frappe.msgprint(_('Comments cannot have links or email addresses')) return False - comments_count = frappe.db.count("Comment", { - "comment_type": "Comment", - "comment_email": comment_email, - "creation": (">", add_to_date(now(), hours=-1)) - }) - - if comments_count > 20: - frappe.msgprint(_('Hourly comment limit reached for: {0}').format(frappe.bold(comment_email))) - return False - comment = doc.add_comment( text=comment, comment_email=comment_email, @@ -54,14 +44,17 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference comment.name, _("View Comment"))) - # notify creator - frappe.sendmail( - recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner, - subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name), - message=content, - reference_doctype=doc.doctype, - reference_name=doc.name - ) + if doc.doctype == "Blog Post" and not doc.enable_email_notification: + pass + else: + # notify creator + frappe.sendmail( + recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner, + subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name), + message=content, + reference_doctype=doc.doctype, + reference_name=doc.name + ) # revert with template if all clear (no backlinks) template = frappe.get_template("templates/includes/comments/comment.html") diff --git a/frappe/templates/includes/feedback/feedback.py b/frappe/templates/includes/feedback/feedback.py index 62fdc3f746..279ff05e6d 100644 --- a/frappe/templates/includes/feedback/feedback.py +++ b/frappe/templates/includes/feedback/feedback.py @@ -12,8 +12,8 @@ from frappe.website.doctype.blog_settings.blog_settings import get_feedback_limi @rate_limit(key='reference_name', limit=get_feedback_limit, seconds=60*60) def give_feedback(reference_doctype, reference_name, like): like = frappe.parse_json(like) - doc = frappe.get_doc(reference_doctype, reference_name) - if doc.disable_feedback == 1: + ref_doc = frappe.get_doc(reference_doctype, reference_name) + if ref_doc.disable_feedback == 1: return filters = { @@ -33,7 +33,7 @@ def give_feedback(reference_doctype, reference_name, like): doc.save(ignore_permissions=True) subject = _('Feedback on {0}: {1}').format(reference_doctype, reference_name) - send_mail(doc, subject) + ref_doc.enable_email_notification and send_mail(doc, subject) return doc def send_mail(feedback, subject): diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index 9400016c48..a7c21b7a48 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -17,6 +17,7 @@ "published", "featured", "hide_cta", + "enable_email_notification", "disable_comments", "disable_feedback", "section_break_5", @@ -197,6 +198,12 @@ "fieldname": "disable_feedback", "fieldtype": "Check", "label": "Disable Feedback" + }, + { + "default": "1", + "fieldname": "enable_email_notification", + "fieldtype": "Check", + "label": "Enable Email Notification" } ], "has_web_view": 1, @@ -206,7 +213,7 @@ "is_published_field": "published", "links": [], "max_attachments": 5, - "modified": "2021-09-13 17:19:35.436045", + "modified": "2021-11-16 16:19:18.135696", "modified_by": "Administrator", "module": "Website", "name": "Blog Post", diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 2b723c107a..3536896a5f 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -104,7 +104,7 @@ class BlogPost(WebsiteGenerator): context.parents = [{"name": _("Home"), "route":"/"}, {"name": "Blog", "route": "/blog"}, {"label": context.category.title, "route":context.category.route}] - context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment", cache=True) + context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment") def fetch_cta(self): if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True): From 6ec6fbaa8de3af7f9586709c6973baad1ca6c264 Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira Date: Tue, 16 Nov 2021 23:28:19 +0530 Subject: [PATCH 19/58] chore: disable eps by default --- .../energy_point_settings.json | 193 ++---------------- .../test_energy_point_settings.py | 8 + 2 files changed, 25 insertions(+), 176 deletions(-) create mode 100644 frappe/social/doctype/energy_point_settings/test_energy_point_settings.py diff --git a/frappe/social/doctype/energy_point_settings/energy_point_settings.json b/frappe/social/doctype/energy_point_settings/energy_point_settings.json index 0001b26529..d1f9aea3d0 100644 --- a/frappe/social/doctype/energy_point_settings/energy_point_settings.json +++ b/frappe/social/doctype/energy_point_settings/energy_point_settings.json @@ -1,229 +1,70 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2019-03-19 13:17:51.710241", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "enabled", + "section_break_2", + "review_levels", + "point_allocation_periodicity", + "last_point_allocation_date" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, + "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "enabled", - "fetch_if_empty": 0, "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "review_levels", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Review Levels", - "length": 0, - "no_copy": 0, - "options": "Review Level", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Review Level" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Weekly", - "fetch_if_empty": 0, "fieldname": "point_allocation_periodicity", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Point Allocation Periodicity", - "length": 0, - "no_copy": 0, - "options": "Daily\nWeekly\nMonthly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Daily\nWeekly\nMonthly" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "last_point_allocation_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last Point Allocation Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, "hide_toolbar": 1, - "idx": 0, - "in_create": 0, - "is_submittable": 0, "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-26 19:10:14.087840", + "links": [], + "modified": "2021-11-16 23:24:01.366928", "modified_by": "Administrator", "module": "Social", "name": "Energy Point Settings", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py b/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py new file mode 100644 index 0000000000..3b0a756878 --- /dev/null +++ b/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestEnergyPointSettings(unittest.TestCase): + pass From 721f6c63da42cfb6ff8a54564d617d2d17baecbd Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 17 Nov 2021 12:54:39 +0530 Subject: [PATCH 20/58] test: report_view.js cypress test fix --- cypress/integration/report_view.js | 4 ++-- frappe/public/js/frappe/list/list_view.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 0253e8fd43..629ae72eb8 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -7,6 +7,8 @@ context('Report View', () => { cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); + }); + it('Field with enabled allow_on_submit should be editable.', () => { cy.insert_doc(doctype_name, { 'title': 'Doc 1', 'description': 'Random Text', @@ -14,8 +16,6 @@ context('Report View', () => { // submit document 'docstatus': 1 }, true).as('doc'); - }); - it('Field with enabled allow_on_submit should be editable.', () => { cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/app/List/${doctype_name}/Report`); // check status column added from docstatus diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 07c8acef27..64530e15ef 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -307,6 +307,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } update_checkbox(target) { + if (!this.$checkbox_actions) return; + let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all"); if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) { From 3129e4d8ab5438945936dc4553362f0ae37109fc Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 17 Nov 2021 13:14:54 +0530 Subject: [PATCH 21/58] feat: Add auto theme switcher - Removed redundant code - Added theme mode change listener to update theme realtime --- frappe/core/doctype/user/user.py | 2 +- frappe/public/js/frappe/desk.js | 17 ++++++--- frappe/public/js/frappe/ui/theme_switcher.js | 37 ++++++++++---------- frappe/public/scss/desk/theme_switcher.scss | 18 ++++++++-- frappe/www/app.html | 2 +- frappe/www/app.py | 2 +- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 86fd1cb4a6..b127cf5f0c 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1046,7 +1046,7 @@ def generate_keys(user): @frappe.whitelist() def switch_theme(theme): - if theme in ["Dark", "Light"]: + if theme in ["Dark", "Light", "Automatic"]: frappe.db.set_value("User", frappe.session.user, "desk_theme", theme) def get_enabled_users(): diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index a00bf01ae7..5a157fcc84 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -64,11 +64,18 @@ frappe.Application = class Application { } }); - if (frappe.boot.desk_theme == 'Automatic') { - frappe.ui.add_system_theme_switch_listener(); - const startup_theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light'; - frappe.ui.toggle_theme(startup_theme); - } + frappe.ui.add_system_theme_switch_listener(); + const root = document.documentElement; + + const observer = new MutationObserver(function(mutations) { + frappe.ui.set_theme(); + }); + observer.observe(root, { + attributes: true, + attributeFilter: ['data-theme-mode'] + }); + + frappe.ui.set_theme(); // page container this.make_page_container(); diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 2a7e09d7c1..4f263e8f1e 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -42,7 +42,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { } refresh() { - this.current_theme = document.documentElement.getAttribute("data-theme") || "light"; + this.current_theme = document.documentElement.getAttribute("data-theme-mode") || "light"; this.fetch_themes().then(() => { this.render(); }); @@ -58,6 +58,11 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { { name: "dark", label: __("Timeless Night"), + }, + { + name: "automatic", + label: __("Automatic"), + info: __("Uses system's theme to switch between light and dark mode") } ]; @@ -75,7 +80,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { get_preview_html(theme) { const preview = $(`
-
+
${frappe.utils.icon('tick', 'xs')}
@@ -112,7 +117,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { toggle_theme(theme, options = { save_preferences: true, show_alert: true }) { this.current_theme = theme.toLowerCase(); - document.documentElement.setAttribute("data-theme", this.current_theme); + document.documentElement.setAttribute("data-theme-mode", this.current_theme); if (options && options.show_alert) { frappe.show_alert("Theme Changed", 3); @@ -134,24 +139,20 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { }; frappe.ui.add_system_theme_switch_listener = function() { - const toggle_theme = frappe.ui.toggle_theme; - frappe.ui.dark_theme_media_query.addEventListener('change', function(e) { - if (e.matches) { - toggle_theme('dark'); - return; - } - - toggle_theme('light'); + frappe.ui.set_theme(); }); }; frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)"); -frappe.ui.toggle_theme = function(theme) { - const theme_switcher = new frappe.ui.ThemeSwitcher(); - theme_switcher.toggle_theme(theme, { - save_preferences: false, - show_alert: false - }); -}; \ No newline at end of file +frappe.ui.set_theme = (theme) => { + const root = document.documentElement; + let theme_mode = root.getAttribute("data-theme-mode"); + if (!theme) { + if (theme_mode === "automatic") { + theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light'; + } + } + root.setAttribute("data-theme", theme || theme_mode); +} \ No newline at end of file diff --git a/frappe/public/scss/desk/theme_switcher.scss b/frappe/public/scss/desk/theme_switcher.scss index 00e3f35be8..c8ff4d3bef 100644 --- a/frappe/public/scss/desk/theme_switcher.scss +++ b/frappe/public/scss/desk/theme_switcher.scss @@ -1,6 +1,6 @@ .modal-body .theme-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); grid-gap: 18px; .background { @@ -9,7 +9,7 @@ border-radius: var(--border-radius-lg); overflow: hidden; cursor: pointer; - height: 160px; + height: 120px; position: relative; &:hover { @@ -72,6 +72,7 @@ border-radius: var(--border-radius-sm); height: 10px; width: 20px; + z-index: 1; } .text { @@ -80,4 +81,17 @@ height: 10px; width: 40px; } +} + +// TODO: Replace with better alternative +[data-theme="automatic"] { + .background::after { + content: ""; + top: 0; + right: 0; + height: 100%; + width: 50%; + background: var(--gray-900); + position: absolute; + } } \ No newline at end of file diff --git a/frappe/www/app.html b/frappe/www/app.html index 68a6dc8e86..37579066e0 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -1,5 +1,5 @@ - + diff --git a/frappe/www/app.py b/frappe/www/app.py index 9bc60cf671..4c8048662a 100644 --- a/frappe/www/app.py +++ b/frappe/www/app.py @@ -45,7 +45,7 @@ def get_context(context): "lang": frappe.local.lang, "sounds": hooks["sounds"], "boot": boot if context.get("for_mobile") else boot_json, - "desk_theme": desk_theme if not desk_theme == 'Automatic' else 'Light', + "desk_theme": desk_theme, "csrf_token": csrf_token, "google_analytics_id": frappe.conf.get("google_analytics_id"), "google_analytics_anonymize_ip": frappe.conf.get("google_analytics_anonymize_ip"), From 1d8cd8e03042daf8dd3c018c5fb48e2cd2df3ef1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 17 Nov 2021 14:03:02 +0530 Subject: [PATCH 22/58] refactor: Revert unnecessary changes and update style --- frappe/public/js/frappe/ui/theme_switcher.js | 26 +++++++++++--------- frappe/public/scss/desk/theme_switcher.scss | 3 ++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 4f263e8f1e..3680089e3c 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -54,10 +54,12 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { { name: "light", label: __("Frappe Light"), + info: __("Light Theme") }, { name: "dark", label: __("Timeless Night"), + info: __("Dark Theme") }, { name: "automatic", @@ -79,11 +81,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { } get_preview_html(theme) { + const is_auto_theme = theme.name === "automatic"; const preview = $(`
-
+
-
${frappe.utils.icon('tick', 'xs')}
+
+ ${frappe.utils.icon('tick', 'xs')} +
@@ -115,20 +121,16 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { return preview; } - toggle_theme(theme, options = { save_preferences: true, show_alert: true }) { + toggle_theme(theme) { this.current_theme = theme.toLowerCase(); document.documentElement.setAttribute("data-theme-mode", this.current_theme); + frappe.show_alert("Theme Changed", 3); - if (options && options.show_alert) { - frappe.show_alert("Theme Changed", 3); - } - - if (options && options.save_preferences) { - frappe.xcall("frappe.core.doctype.user.user.switch_theme", { - theme: toTitle(theme) - }); - } + frappe.xcall("frappe.core.doctype.user.user.switch_theme", { + theme: toTitle(theme) + }); } + show() { this.dialog.show(); } diff --git a/frappe/public/scss/desk/theme_switcher.scss b/frappe/public/scss/desk/theme_switcher.scss index c8ff4d3bef..924c2edd9d 100644 --- a/frappe/public/scss/desk/theme_switcher.scss +++ b/frappe/public/scss/desk/theme_switcher.scss @@ -28,6 +28,7 @@ margin-right: var(--margin-sm); border-radius: var(--border-radius-full); + z-index: 1; } } @@ -84,7 +85,7 @@ } // TODO: Replace with better alternative -[data-theme="automatic"] { +[data-is-auto-theme="true"] { .background::after { content: ""; top: 0; From 3be669ed69869766ca339cefd04f23123384c3e4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 17 Nov 2021 14:07:20 +0530 Subject: [PATCH 23/58] feat(ux): option to disable EPS notifications (#14992) --- .../notification_settings.json | 20 ++++++++++++++++--- .../energy_point_log/energy_point_log.py | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index fc12022e89..fc535fa405 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -15,7 +15,9 @@ "enable_email_energy_point", "enable_email_share", "user", - "seen" + "seen", + "system_notifications_section", + "energy_points_system_notifications" ], "fields": [ { @@ -84,15 +86,27 @@ "fieldtype": "Check", "hidden": 1, "label": "Seen" + }, + { + "fieldname": "system_notifications_section", + "fieldtype": "Section Break", + "label": "System Notifications" + }, + { + "default": "1", + "fieldname": "energy_points_system_notifications", + "fieldtype": "Check", + "label": "Energy Points" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-04 12:54:57.989317", + "modified": "2021-11-16 12:18:46.955501", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -111,4 +125,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 3ffabcd241..86843302e9 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -32,7 +32,9 @@ class EnergyPointLog(Document): frappe.cache().hdel('energy_points', self.user) frappe.publish_realtime('update_points', after_commit=True) - if self.type != 'Review': + if self.type != 'Review' and \ + frappe.get_cached_value('Notification Settings', self.user, 'energy_points_system_notifications'): + reference_user = self.user if self.type == 'Auto' else self.owner notification_doc = { 'type': 'Energy Point', From 69c87e5caec236ed2b09f52dec73c5c6a2426f9d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 17 Nov 2021 14:47:41 +0530 Subject: [PATCH 24/58] fix: optimise `mark_email_as_seen` --- frappe/core/doctype/communication/email.py | 54 +++++++++++++++------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 4d22075b78..2b6ae80de6 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -147,24 +147,44 @@ def add_attachments(name, attachments): _file.save(ignore_permissions=True) @frappe.whitelist(allow_guest=True) -def mark_email_as_seen(name=None): +def mark_email_as_seen(name: str = None): try: - if name and frappe.db.exists("Communication", name) and not frappe.db.get_value("Communication", name, "read_by_recipient"): - frappe.db.set_value("Communication", name, "read_by_recipient", 1) - frappe.db.set_value("Communication", name, "delivery_status", "Read") - frappe.db.set_value("Communication", name, "read_by_recipient_on", get_datetime()) - frappe.db.commit() + update_communication_as_seen(name) + except Exception: frappe.log_error(frappe.get_traceback()) - finally: - # Return image as response under all circumstances - from PIL import Image - import io - im = Image.new('RGBA', (1, 1)) - im.putdata([(255,255,255,0)]) - buffered_obj = io.BytesIO() - im.save(buffered_obj, format="PNG") - frappe.response["type"] = 'binary' - frappe.response["filename"] = "imaginary_pixel.png" - frappe.response["filecontent"] = buffered_obj.getvalue() + finally: + frappe.response.update({ + "type": "binary", + "filename": "imaginary_pixel.png", + "filecontent": ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" + b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" + b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" + b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" + ) + }) + +def update_communication_as_seen(name): + if not name or not isinstance(name, str): + return + + values = frappe.db.get_value( + "Communication", + name, + "read_by_recipient", + as_dict=True + ) + + # Communication not found or already marked read + if not values or values.read_by_recipient: + return + + frappe.db.set_value("Communication", name, { + "read_by_recipient": 1, + "delivery_status": "Read", + "read_by_recipient_on": get_datetime() + }) + + frappe.db.commit() From a80cf47426947651315151cc989211eae7ba23bf Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 17 Nov 2021 14:55:20 +0530 Subject: [PATCH 25/58] fix: enforce GET method --- frappe/core/doctype/communication/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 2b6ae80de6..5a5d32022b 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -146,7 +146,7 @@ def add_attachments(name, attachments): }) _file.save(ignore_permissions=True) -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=("GET",)) def mark_email_as_seen(name: str = None): try: update_communication_as_seen(name) From fc183e3767b2e5e3fd1a7270e802218a4e726a40 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 17 Nov 2021 15:31:03 +0530 Subject: [PATCH 26/58] style: Fix formatting issues --- frappe/public/js/frappe/desk.js | 2 +- frappe/public/js/frappe/ui/theme_switcher.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 5a157fcc84..2855c6ae7c 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -67,7 +67,7 @@ frappe.Application = class Application { frappe.ui.add_system_theme_switch_listener(); const root = document.documentElement; - const observer = new MutationObserver(function(mutations) { + const observer = new MutationObserver(() => { frappe.ui.set_theme(); }); observer.observe(root, { diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 3680089e3c..2c1d93a2ec 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -140,8 +140,8 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { } }; -frappe.ui.add_system_theme_switch_listener = function() { - frappe.ui.dark_theme_media_query.addEventListener('change', function(e) { +frappe.ui.add_system_theme_switch_listener = () => { + frappe.ui.dark_theme_media_query.addEventListener('change', () => { frappe.ui.set_theme(); }); }; @@ -157,4 +157,4 @@ frappe.ui.set_theme = (theme) => { } } root.setAttribute("data-theme", theme || theme_mode); -} \ No newline at end of file +}; \ No newline at end of file From d79450c501cbaeb6526fd54d1cf2e57c8b3861a3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 17 Nov 2021 15:37:28 +0530 Subject: [PATCH 27/58] ci: add timeout to CI jobs (#15000) once a day some job gets stuck and default timeout is 6 hours. Changed timeout to 1 hour which is 3-4x more than max running time of all jobs. --- .github/workflows/patch-mariadb-tests.yml | 1 + .github/workflows/server-mariadb-tests.yml | 3 ++- .github/workflows/server-postgres-tests.yml | 1 + .github/workflows/ui-tests.yml | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index d9a6ca6f59..52fa987994 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -10,6 +10,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 name: Patch Test diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml index 588f357f26..4edf74ba71 100644 --- a/.github/workflows/server-mariadb-tests.yml +++ b/.github/workflows/server-mariadb-tests.yml @@ -14,6 +14,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false @@ -128,4 +129,4 @@ jobs: fail_ci_if_error: true files: /home/runner/frappe-bench/sites/coverage.xml verbose: true - flags: server \ No newline at end of file + flags: server diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml index 78f379837b..895af5184e 100644 --- a/.github/workflows/server-postgres-tests.yml +++ b/.github/workflows/server-postgres-tests.yml @@ -13,6 +13,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index fcc53ba59c..cb502f68a7 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -13,6 +13,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false From 7b7f74ce23c2d9af7d5d6ee01e209385778aa51c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 17 Nov 2021 15:44:49 +0530 Subject: [PATCH 28/58] fix: move commit call to `mark_email_as_seen` --- frappe/core/doctype/communication/email.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 5a5d32022b..90b48ea4b4 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -150,6 +150,7 @@ def add_attachments(name, attachments): def mark_email_as_seen(name: str = None): try: update_communication_as_seen(name) + frappe.db.commit() # nosemgrep: this will be called in a GET request except Exception: frappe.log_error(frappe.get_traceback()) @@ -186,5 +187,3 @@ def update_communication_as_seen(name): "delivery_status": "Read", "read_by_recipient_on": get_datetime() }) - - frappe.db.commit() From 673c8baa2ac7e352fa42bfb73aad3597e4604d87 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 17 Nov 2021 15:53:46 +0530 Subject: [PATCH 29/58] test: fix EPS test after changing default value --- .../energy_point_log/test_energy_point_log.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frappe/social/doctype/energy_point_log/test_energy_point_log.py b/frappe/social/doctype/energy_point_log/test_energy_point_log.py index c2bcbde825..a1f4503c34 100644 --- a/frappe/social/doctype/energy_point_log/test_energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/test_energy_point_log.py @@ -8,6 +8,18 @@ from frappe.utils.testutils import add_custom_field, clear_custom_fields from frappe.desk.form.assign_to import add as assign_to class TestEnergyPointLog(unittest.TestCase): + @classmethod + def setUpClass(cls): + settings = frappe.get_single('Energy Point Settings') + settings.enabled = 1 + settings.save() + + @classmethod + def tearDownClass(cls): + settings = frappe.get_single('Energy Point Settings') + settings.enabled = 0 + settings.save() + def setUp(self): frappe.cache().delete_value('energy_point_rule_map') @@ -336,4 +348,4 @@ def assign_users_to_todo(todo_name, users): 'assign_to': [user], 'doctype': 'ToDo', 'name': todo_name - }) \ No newline at end of file + }) From 7aa578d992eb287607b858f775b6d701203a4c99 Mon Sep 17 00:00:00 2001 From: Mitul David Date: Wed, 17 Nov 2021 16:23:45 +0530 Subject: [PATCH 30/58] refactor: Display errors in FilePreview --- frappe/core/doctype/file/file.py | 4 +- .../js/frappe/file_uploader/FilePreview.vue | 22 +++++++--- .../js/frappe/file_uploader/FileUploader.vue | 43 ++++++++++++++++--- .../public/js/frappe/file_uploader/index.js | 12 ++++-- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 4df9ef3132..0021240106 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -716,13 +716,11 @@ def delete_file(path): os.remove(path) - - +@frappe.whitelist() def get_max_file_size(): return cint(conf.get('max_file_size')) or 10485760 - def has_permission(doc, ptype=None, user=None): has_access = False user = user or frappe.session.user diff --git a/frappe/public/js/frappe/file_uploader/FilePreview.vue b/frappe/public/js/frappe/file_uploader/FilePreview.vue index 43dbacb17d..76c3537e79 100644 --- a/frappe/public/js/frappe/file_uploader/FilePreview.vue +++ b/frappe/public/js/frappe/file_uploader/FilePreview.vue @@ -29,10 +29,15 @@
+
+ + {{ file.error_message }} + +
-
+
- +
@@ -89,18 +94,18 @@ export default { return this.file.doc ? this.file.doc.is_private : this.file.private; }, uploaded() { - return this.file.total && this.file.total === this.file.progress && !this.file.failed; + return this.file.request_succeeded; }, is_image() { return this.file.file_obj.type.startsWith('image'); }, is_optimizable() { let is_svg = this.file.file_obj.type == 'image/svg+xml'; - return this.is_image && !is_svg; + return this.is_image && !is_svg && !this.uploaded && !this.file.failed; }, is_cropable() { let croppable_types = ['image/jpeg', 'image/png']; - return !this.uploaded && !this.file.uploading && croppable_types.includes(this.file.file_obj.type); + return !this.uploaded && !this.file.uploading && !this.file.failed && croppable_types.includes(this.file.file_obj.type); }, progress() { let value = Math.round((this.file.progress * 100) / this.file.total); @@ -208,4 +213,9 @@ export default { align-items: center; padding-top: 0.25rem; } + +.file-error { + font-size: var(--text-sm); + font-weight: var(--text-bold); +} diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 90aa545941..8d93052cd3 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -197,6 +197,7 @@ export default { show_image_cropper: false, crop_image_with_index: -1, trigger_upload: false, + close_dialog: false, hide_dialog_footer: false, allow_take_photo: false, allow_web_link: true, @@ -218,6 +219,12 @@ export default { } }); } + if (this.restrictions.max_file_size == null) { + frappe.call('frappe.core.doctype.file.file.get_max_file_size') + .then(res => { + this.restrictions.max_file_size = Number(res.message); + }); + } }, watch: { files(newvalue, oldvalue) { @@ -289,6 +296,8 @@ export default { progress: 0, total: 0, failed: false, + request_succeeded: false, + error_message: null, uploading: false, private: !is_image } @@ -329,9 +338,17 @@ export default { if (!is_correct_type) { console.warn('File skipped because of invalid file type', file); + frappe.show_alert({ + message:__(`File "${file.name}" was skipped because of invalid file type`), + indicator:'orange' + }); } if (!valid_file_size) { console.warn('File skipped because of invalid file size', file.size, file); + frappe.show_alert({ + message:__(`File "${file.name}" was skipped because size exceeds ${max_file_size / (1024 * 1024)} MB`), + indicator:'orange' + }); } return is_correct_type && valid_file_size; @@ -357,9 +374,10 @@ export default { let selected_file = this.$refs.file_browser.selected_node; if (!selected_file.value) { frappe.msgprint(__('Click on a file to select it.')); + this.close_dialog = true; return Promise.reject(); } - + this.close_dialog = true; return this.upload_file({ file_url: selected_file.file_url }); @@ -368,9 +386,11 @@ export default { let file_url = this.$refs.web_link.url; if (!file_url) { frappe.msgprint(__('Invalid URL')); + this.close_dialog = true; return Promise.reject(); } file_url = decodeURI(file_url) + this.close_dialog = true; return this.upload_file({ file_url }); @@ -383,6 +403,7 @@ export default { this.on_success && this.on_success(file); }) ); + this.close_dialog = true; return Promise.all(promises); }, upload_file(file, i) { @@ -410,6 +431,7 @@ export default { xhr.onreadystatechange = () => { if (xhr.readyState == XMLHttpRequest.DONE) { if (xhr.status === 200) { + file.request_succeeded = true; let r = null; let file_doc = null; try { @@ -426,15 +448,24 @@ export default { if (this.on_success) { this.on_success(file_doc, r); } + + if (i == this.files.length - 1 && this.files.every(file => file.request_succeeded)) { + this.close_dialog = true; + } + } else if (xhr.status === 403) { + file.failed = true; let response = JSON.parse(xhr.responseText); - frappe.msgprint({ - title: __('Not permitted'), - indicator: 'red', - message: response._error_message - }); + file.error_message = `Not permitted. ${response._error_message || ''}`; + + } else if (xhr.status === 413) { + file.failed = true; + file.error_message = 'Size exceeds the maximum allowed file size.'; + } else { file.failed = true; + file.error_message = xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`; + let error = null; try { error = JSON.parse(xhr.responseText); diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js index 87bc1c8ec8..ec90b19a1a 100644 --- a/frappe/public/js/frappe/file_uploader/index.js +++ b/frappe/public/js/frappe/file_uploader/index.js @@ -67,6 +67,12 @@ export default class FileUploader { } }); + this.uploader.$watch('close_dialog', (close_dialog) => { + if (close_dialog) { + this.dialog && this.dialog.hide(); + } + }); + this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => { if (hide_dialog_footer) { this.dialog && this.dialog.footer.addClass('hide'); @@ -84,10 +90,8 @@ export default class FileUploader { upload_files() { this.dialog && this.dialog.get_primary_btn().prop('disabled', true); - return this.uploader.upload_files() - .then(() => { - this.dialog && this.dialog.hide(); - }); + this.dialog && this.dialog.get_secondary_btn().prop('disabled', true); + return this.uploader.upload_files(); } make_dialog() { From 648d24eca58fa97eb0805e24b70552fe54a31566 Mon Sep 17 00:00:00 2001 From: Mitul David Date: Wed, 17 Nov 2021 16:26:00 +0530 Subject: [PATCH 31/58] refactor: Limit file size of uploads by setting max_content_length --- frappe/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/app.py b/frappe/app.py index 8e1534e7ef..70575fe2f1 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -120,6 +120,8 @@ def init_request(request): else: frappe.connect(set_admin_as_user=False) + request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024 + make_form_dict(request) if request.method != "OPTIONS": From 8b343f27d04eaa54460f939d590b8751ffa4b893 Mon Sep 17 00:00:00 2001 From: Mitul David Date: Wed, 17 Nov 2021 16:27:46 +0530 Subject: [PATCH 32/58] fix: Invalid prop - type check failed in ProgressRing --- frappe/public/js/frappe/file_uploader/FilePreview.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/file_uploader/FilePreview.vue b/frappe/public/js/frappe/file_uploader/FilePreview.vue index 76c3537e79..5972a975f2 100644 --- a/frappe/public/js/frappe/file_uploader/FilePreview.vue +++ b/frappe/public/js/frappe/file_uploader/FilePreview.vue @@ -40,9 +40,9 @@ v-show="file.uploading && !uploaded && !file.failed" primary="var(--primary-color)" secondary="var(--gray-200)" - radius="24" + :radius="24" :progress="progress" - stroke="3" + :stroke="3" />
From 04b21919260cc12b89655cd9035e6a43b21bf6a8 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 17 Nov 2021 16:29:14 +0530 Subject: [PATCH 33/58] fix: broken collapsible section, + button missing, misplaced message section --- frappe/public/js/frappe/form/dashboard.js | 10 ++++++++-- frappe/public/js/frappe/form/form.js | 7 +++++-- frappe/public/js/frappe/form/layout.js | 2 +- frappe/public/js/frappe/form/section.js | 4 +++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 9d5e7cbe09..df4dbf09e7 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -14,6 +14,7 @@ frappe.ui.form.Dashboard = class FormDashboard { this.progress_area = this.make_section({ css_class: 'progress-area', hidden: 1, + collapsible: 1, is_dashboard_section: 1, }); @@ -21,6 +22,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Overview"), css_class: 'form-heatmap', hidden: 1, + collapsible: 1, is_dashboard_section: 1, body_html: `
@@ -32,6 +34,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Graph"), css_class: 'form-graph', hidden: 1, + collapsible: 1, is_dashboard_section: 1 }); @@ -40,6 +43,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Stats"), css_class: 'form-stats', hidden: 1, + collapsible: 1, is_dashboard_section: 1, body_html: this.stats_area_row }); @@ -50,6 +54,7 @@ frappe.ui.form.Dashboard = class FormDashboard { label: __("Connections"), css_class: 'form-links', hidden: 1, + collapsible: 1, is_dashboard_section: 1, body_html: this.transactions_area }); @@ -84,9 +89,10 @@ frappe.ui.form.Dashboard = class FormDashboard { hidden, body_html, make_card: true, + collapsible: 1, is_dashboard_section: 1 }; - return new Section(this.frm.layout.wrapper, options).body; + return new Section(this.parent, options).body; } add_progress(title, percent, message) { @@ -203,7 +209,7 @@ frappe.ui.form.Dashboard = class FormDashboard { after_refresh() { // show / hide new buttons (if allowed) this.links_area.body.find('.btn-new').each((i, el) => { - if (this.frm.can_create($(this).attr('data-doctype'))) { + if (this.frm.can_create($(el).attr('data-doctype'))) { $(el).removeClass('hidden'); } }); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 75d68b12db..27281d8927 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -156,8 +156,11 @@ frappe.ui.form.Form = class FrappeForm { let dashboard_parent = $('
'); - let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper; - main_page.prepend(dashboard_parent); + if (this.layout.tabs.length) { + this.layout.tabs[0].wrapper.prepend(dashboard_parent); + } else { + dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message')); + } this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this); this.tour = new frappe.ui.form.FormTour({ diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 7710c82ee7..0de6b1db0d 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -245,7 +245,7 @@ frappe.ui.form.Layout = class Layout { } make_section(df) { - this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout); + this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout, this); // append to layout fields if (df) { diff --git a/frappe/public/js/frappe/form/section.js b/frappe/public/js/frappe/form/section.js index e0120f6afc..601b541afe 100644 --- a/frappe/public/js/frappe/form/section.js +++ b/frappe/public/js/frappe/form/section.js @@ -1,5 +1,6 @@ export default class Section { - constructor(parent, df, card_layout) { + constructor(parent, df, card_layout, layout) { + this.layout = layout; this.card_layout = card_layout; this.parent = parent; this.df = df || {}; @@ -25,6 +26,7 @@ export default class Section { ${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"} ${ make_card ? "card-section" : "" }"> `).appendTo(this.parent); + this.layout && this.layout.sections.push(this); if (this.df) { if (this.df.label) { From cb97b49c78ef9df61eb683d09d262651a738b392 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 17 Nov 2021 16:34:47 +0530 Subject: [PATCH 34/58] fix: slightly better naming --- frappe/core/doctype/communication/email.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 90b48ea4b4..54ddbce2c4 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -149,7 +149,7 @@ def add_attachments(name, attachments): @frappe.whitelist(allow_guest=True, methods=("GET",)) def mark_email_as_seen(name: str = None): try: - update_communication_as_seen(name) + update_communication_as_read(name) frappe.db.commit() # nosemgrep: this will be called in a GET request except Exception: @@ -167,19 +167,18 @@ def mark_email_as_seen(name: str = None): ) }) -def update_communication_as_seen(name): +def update_communication_as_read(name): if not name or not isinstance(name, str): return - values = frappe.db.get_value( + communication = frappe.db.get_value( "Communication", name, "read_by_recipient", as_dict=True ) - # Communication not found or already marked read - if not values or values.read_by_recipient: + if not communication or communication.read_by_recipient: return frappe.db.set_value("Communication", name, { From 0aaf9063564a973f3f95be65e462330d88aa056d Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 17 Nov 2021 16:46:06 +0530 Subject: [PATCH 35/58] fix: sider fix --- frappe/public/js/frappe/form/section.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/section.js b/frappe/public/js/frappe/form/section.js index 601b541afe..b0ec491ce6 100644 --- a/frappe/public/js/frappe/form/section.js +++ b/frappe/public/js/frappe/form/section.js @@ -26,7 +26,7 @@ export default class Section { ${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"} ${ make_card ? "card-section" : "" }"> `).appendTo(this.parent); - this.layout && this.layout.sections.push(this); + this.layout && this.layout.sections.push(this); if (this.df) { if (this.df.label) { From 58f2296c49f19a7d1db14ba30be4d7b49be14245 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 17 Nov 2021 17:52:08 +0530 Subject: [PATCH 36/58] fix: Add fallback to desk theme to avoid failure --- frappe/www/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/www/app.py b/frappe/www/app.py index 4c8048662a..92107816c7 100644 --- a/frappe/www/app.py +++ b/frappe/www/app.py @@ -45,7 +45,7 @@ def get_context(context): "lang": frappe.local.lang, "sounds": hooks["sounds"], "boot": boot if context.get("for_mobile") else boot_json, - "desk_theme": desk_theme, + "desk_theme": desk_theme or "Light", "csrf_token": csrf_token, "google_analytics_id": frappe.conf.get("google_analytics_id"), "google_analytics_anonymize_ip": frappe.conf.get("google_analytics_anonymize_ip"), From 3ee9c0492a0c91663e191e8e4310a477f6f22aa7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 17 Nov 2021 19:40:38 +0530 Subject: [PATCH 37/58] test: print view should not show warning/errors (#14972) --- frappe/tests/test_printview.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frappe/tests/test_printview.py diff --git a/frappe/tests/test_printview.py b/frappe/tests/test_printview.py new file mode 100644 index 0000000000..0fc4c4869b --- /dev/null +++ b/frappe/tests/test_printview.py @@ -0,0 +1,22 @@ +import unittest + +import frappe +from frappe.www.printview import get_html_and_style + + +class PrintViewTest(unittest.TestCase): + def test_print_view_without_errors(self): + + user = frappe.get_last_doc("User") + + messages_before = frappe.get_message_log() + ret = get_html_and_style(doc=user.as_json(), print_format="Standard", no_letterhead=1) + messages_after = frappe.get_message_log() + + if len(messages_after) > len(messages_before): + new_messages = messages_after[len(messages_before):] + self.fail("Print view showing error/warnings: \n" + + "\n".join(str(msg) for msg in new_messages)) + + # html should exist + self.assertTrue(bool(ret["html"])) From bd61ae4675e96be3eb8e7e5bbbbfc531b673b74d Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 17 Nov 2021 20:08:30 +0530 Subject: [PATCH 38/58] fix: only allow group_by for list view data --- frappe/public/js/frappe/list/base_list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 03e20ee6f5..648cb8b94f 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -408,6 +408,7 @@ frappe.views.BaseList = class BaseList { } get_group_by() { + if (this.view && this.view !== 'List') return; let name_field = this.fields && this.fields.find(f => f[0] == 'name'); if (name_field) { return frappe.model.get_full_column_name(name_field[0], name_field[1]); From 26a49d5f148d08cafbd18e23a322283ff6b74241 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 17 Nov 2021 20:42:31 +0530 Subject: [PATCH 39/58] fix: moved report view related code to report_view.js --- frappe/public/js/frappe/list/base_list.js | 1 - frappe/public/js/frappe/views/reports/report_view.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 648cb8b94f..03e20ee6f5 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -408,7 +408,6 @@ frappe.views.BaseList = class BaseList { } get_group_by() { - if (this.view && this.view !== 'List') return; let name_field = this.fields && this.fields.find(f => f[0] == 'name'); if (name_field) { return frappe.model.get_full_column_name(name_field[0], name_field[1]); diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 8866a4b2af..86b46d77a3 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -106,6 +106,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { get_args() { const args = super.get_args(); + args.group_by = null; this.group_by_control.set_args(args); return args; From 452146e5640ff2a974442555377d68ad2324930b Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 18 Nov 2021 14:13:30 +0530 Subject: [PATCH 40/58] fix: Only show report when there is data(removed from hide_loading_screen method) --- frappe/public/js/frappe/views/reports/query_report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 04cc1b9880..dbf33d7058 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -634,6 +634,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.render_datatable(); this.add_chart_buttons_to_toolbar(true); this.add_card_button_to_toolbar(); + this.$report.show(); } else { this.data = []; this.toggle_nothing_to_show(true); @@ -882,7 +883,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { hide_loading_screen() { this.$loading.hide(); - this.$report.show(); } get_chart_options(data) { From ed312fc22e86fca80502baceb5ab9722c820355e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 18 Nov 2021 15:28:54 +0530 Subject: [PATCH 41/58] fix: change subject data type from Data to Text Subject can be longer than 140 characters. No real reason to restrict it to arbirary length. PS: This is done because events are created from other doctypes automatically and longer subject lines causes a failure where user can't do much to recover from it. --- frappe/desk/doctype/event/event.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 5768f00f32..2f67c36fc0 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -53,7 +53,7 @@ }, { "fieldname": "subject", - "fieldtype": "Data", + "fieldtype": "Small Text", "in_global_search": 1, "in_list_view": 1, "label": "Subject", @@ -277,10 +277,11 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2020-01-14 21:47:15.825287", + "modified": "2021-11-18 05:06:24.881742", "modified_by": "Administrator", "module": "Desk", "name": "Event", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { From ba7024ca67b16be5967f7d009cc7cf0eadbbb5a6 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 18 Nov 2021 17:11:12 +0530 Subject: [PATCH 42/58] fix: deleting group_by argument --- frappe/public/js/frappe/views/reports/report_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 86b46d77a3..c26b63a9f6 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -106,7 +106,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { get_args() { const args = super.get_args(); - args.group_by = null; + delete args.group_by; this.group_by_control.set_args(args); return args; From abebd29ef4078737d362986d4b77eaa4c6045887 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 18 Nov 2021 17:16:20 +0530 Subject: [PATCH 43/58] test: fix expected failing test --- frappe/tests/test_document.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 47ad029274..29cec8b230 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -157,7 +157,7 @@ class TestDocument(unittest.TestCase): def test_varchar_length(self): d = self.test_insert() - d.subject = "abcde"*100 + d.sender = "abcde"*100 + "@user.com" self.assertRaises(frappe.CharacterLengthExceededError, d.save) def test_xss_filter(self): @@ -251,4 +251,4 @@ class TestDocument(unittest.TestCase): 'doctype': 'Test Formatted', 'currency': 100000 }) - self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') \ No newline at end of file + self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') From 16dca0b1c1c9293f0afe80127341f58f747f38df Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt Date: Mon, 18 Oct 2021 14:04:53 +0200 Subject: [PATCH 44/58] feat: add consecutive calendar week (WW) for naming series The calendar week is based on ISO 8601 but behaves slightly different for the first and last days of a year to ensure consecutiveness: * If the first days of a year would be in week 53 then 00 is used instead. * if the last days of a year would be in week 01 then 53 is used instead. Closes #14413 --- frappe/model/naming.py | 15 ++++++++++ frappe/tests/test_naming.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/frappe/model/naming.py b/frappe/model/naming.py index deea6698b3..f3d68f3715 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -175,6 +175,8 @@ def parse_naming_series(parts, doctype='', doc=''): part = today.strftime("%d") elif e == 'YYYY': part = today.strftime('%Y') + elif e == 'WW': + part = determine_consecutive_week_number(today) elif e == 'timestamp': part = str(today) elif e == 'FY': @@ -193,6 +195,19 @@ def parse_naming_series(parts, doctype='', doc=''): return n +def determine_consecutive_week_number(datetime): + """Determines the consecutive calendar week""" + m = datetime.month + # ISO 8601 calandar week + w = datetime.strftime('%V') + # Ensure consecutiveness for the first and last days of a year + if m == 1 and int(w) >= 52: + w = '00' + elif m == 12 and int(w) <= 1: + w = '53' + return w + + def getseries(key, digits): # series created ? # Using frappe.qb as frappe.get_values does not allow order_by=None diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 4435a8bb20..3031d3e344 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -7,6 +7,7 @@ from frappe.utils import now_datetime from frappe.model.naming import getseries from frappe.model.naming import append_number_if_name_exists, revert_series_if_last +from frappe.model.naming import determine_consecutive_week_number, parse_naming_series class TestNaming(unittest.TestCase): def tearDown(self): @@ -60,6 +61,34 @@ class TestNaming(unittest.TestCase): self.assertEqual(todo.name, 'TODO-{month}-{status}-{series}'.format( month=now_datetime().strftime('%m'), status=todo.status, series=series)) + def test_format_autoname_for_consecutive_week_number(self): + ''' + Test if braced params are replaced for consecutive week number in format autoname + ''' + doctype = 'ToDo' + + todo_doctype = frappe.get_doc('DocType', doctype) + todo_doctype.autoname = 'format:TODO-{WW}-{##}' + todo_doctype.save() + + description = 'Format' + + todo = frappe.new_doc(doctype) + todo.description = description + todo.insert() + + series = getseries('', 2) + + series = str(int(series)-1) + + if len(series) < 2: + series = '0' + series + + week = determine_consecutive_week_number(now_datetime()) + + self.assertEqual(todo.name, 'TODO-{week}-{series}'.format( + week=week, series=series)) + def test_revert_series(self): from datetime import datetime year = datetime.now().year @@ -150,3 +179,32 @@ class TestNaming(unittest.TestCase): self.assertEqual(amended_doc.name, "{}-CANC-1".format(original_name)) submittable_doctype.delete() + + def test_parse_naming_series_for_consecutive_week_number(self): + week = determine_consecutive_week_number(now_datetime()) + name = parse_naming_series('PREFIX-.WW.-SUFFIX') + expected_name = 'PREFIX-{}-SUFFIX'.format(week) + self.assertEqual(name, expected_name) + + def test_determine_consecutive_week_number(self): + from datetime import datetime + + dt = datetime.fromisoformat("2019-12-31") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "53") + + dt = datetime.fromisoformat("2020-01-01") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "01") + + dt = datetime.fromisoformat("2020-01-15") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "03") + + dt = datetime.fromisoformat("2021-01-01") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "00") + + dt = datetime.fromisoformat("2021-12-31") + w = determine_consecutive_week_number(dt) + self.assertEqual(w, "52") From ce7392cdc6922798d6b8793ed573ead314169a44 Mon Sep 17 00:00:00 2001 From: Mohammed Redah Date: Sat, 20 Nov 2021 19:01:05 +0300 Subject: [PATCH 45/58] fix: Translate Strings --- frappe/public/js/frappe/ui/toolbar/awesome_bar.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index b85de18be2..1e131bbe5b 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -307,7 +307,7 @@ frappe.search.AwesomeBar = class AwesomeBar { index: 80, default: "Calculator", onclick: function() { - frappe.msgprint(formatted_value, "Result"); + frappe.msgprint(formatted_value, __("Result")); } }); } catch(e) { @@ -319,10 +319,10 @@ frappe.search.AwesomeBar = class AwesomeBar { make_random(txt) { if(txt.toLowerCase().includes('random')) { this.options.push({ - label: "Generate Random Password", + label: __("Generate Random Password"), value: frappe.utils.get_random(16), onclick: function() { - frappe.msgprint(frappe.utils.get_random(16), "Result"); + frappe.msgprint(frappe.utils.get_random(16), __("Result")); } }) } From f3aabb5211424b398fadf79554e14ea62b2a411a Mon Sep 17 00:00:00 2001 From: tahir-zaqout <64440620+tahir-zaqout@users.noreply.github.com> Date: Mon, 22 Nov 2021 07:55:19 +0200 Subject: [PATCH 46/58] Add Translate To Symbol Field (#14971) --- frappe/public/js/frappe/utils/number_format.js | 2 +- frappe/public/js/frappe/widgets/number_card_widget.js | 2 +- frappe/utils/data.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 32e3669caf..b0d66ccec5 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -129,7 +129,7 @@ function format_currency(v, currency, decimals) { } if (symbol) - return symbol + " " + format_number(v, format, decimals); + return __(symbol) + " " + format_number(v, format, decimals); else return format_number(v, format, decimals); } diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 82d056bb31..11e567af42 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -211,7 +211,7 @@ export default class NumberCardWidget extends Widget { const symbol = number_parts[1] || ''; const formatted_number = $(frappe.format(number_parts[0], df)).text(); - this.formatted_number = formatted_number + ' ' + symbol; + this.formatted_number = formatted_number + ' ' + __(symbol); } render_number() { diff --git a/frappe/utils/data.py b/frappe/utils/data.py index f5c46dc184..d39d32d8df 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -868,7 +868,7 @@ def fmt_money(amount, precision=None, currency=None, format=None): if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes": symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency - amount = symbol + " " + amount + amount = frappe._(symbol) + " " + amount return amount From 13c75c805f6559ae467d78b3ebbeada271ba80e5 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 22 Nov 2021 12:48:04 +0530 Subject: [PATCH 47/58] fix: set `Script Manager` as a standard role --- frappe/core/doctype/role/role.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 98d2d72fc2..389e18dd4c 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -2,15 +2,22 @@ # License: MIT. See LICENSE import frappe - from frappe.model.document import Document desk_properties = ("search_bar", "notifications", "list_sidebar", "bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard") +STANDARD_ROLES = ( + "Administrator", + "System Manager", + "Script Manager", + "All", + "Guest" +) + class Role(Document): def before_rename(self, old, new, merge=False): - if old in ("Guest", "Administrator", "System Manager", "All"): + if old in STANDARD_ROLES: frappe.throw(frappe._("Standard roles cannot be renamed")) def after_insert(self): @@ -23,7 +30,7 @@ class Role(Document): self.set_desk_properties() def disable_role(self): - if self.name in ("Guest", "Administrator", "System Manager", "All"): + if self.name in STANDARD_ROLES: frappe.throw(frappe._("Standard roles cannot be disabled")) else: self.remove_roles() From 85fe86c37043c3ceb32cb02bbf0091a595c5abd1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 22 Nov 2021 13:28:44 +0530 Subject: [PATCH 48/58] fix: Use `get_all` instead of `get_list` for child doctype This change is required to avoid permission error --- frappe/core/doctype/data_import/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 684328a4c7..21faf98e49 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -191,7 +191,7 @@ class Exporter: [format_column_name(df) for df in self.fields if df.parent == child_table_doctype] ) ) - data = frappe.db.get_list( + data = frappe.db.get_all( child_table_doctype, filters={ "parent": ("in", parent_names), From 99abb51fb68d7da8d3006a037a436aa5e2baa6a7 Mon Sep 17 00:00:00 2001 From: Summayya Date: Mon, 22 Nov 2021 14:15:47 +0530 Subject: [PATCH 49/58] refactor: 404 page --- frappe/public/images/ui-states/404.png | Bin 0 -> 96089 bytes frappe/public/scss/website/error-state.scss | 18 ++++++++++ frappe/public/scss/website/index.scss | 1 + frappe/www/404.html | 36 +++++++------------- 4 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 frappe/public/images/ui-states/404.png create mode 100644 frappe/public/scss/website/error-state.scss diff --git a/frappe/public/images/ui-states/404.png b/frappe/public/images/ui-states/404.png new file mode 100644 index 0000000000000000000000000000000000000000..1cbf7eeee0a3035756fd1e9bc6bb06276abfdfb4 GIT binary patch literal 96089 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
  • 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 literal 0 HcmV?d00001 diff --git a/frappe/public/scss/website/error-state.scss b/frappe/public/scss/website/error-state.scss new file mode 100644 index 0000000000..f3fcc140d6 --- /dev/null +++ b/frappe/public/scss/website/error-state.scss @@ -0,0 +1,18 @@ +.error-page { + text-align: center; + + .img-404{ + width: 40%; + margin: var(--margin-2xl) auto; + + @include media-breakpoint-down(sm) { + width: 80% + } + } + + .back-to-home { + font-size: var(--text-base); + } +} + + diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 8e98333306..9cff2f2d77 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -26,6 +26,7 @@ @import 'doc'; @import 'navbar'; @import 'footer'; +@import 'error-state'; .ql-editor.read-mode { padding: 0; diff --git a/frappe/www/404.html b/frappe/www/404.html index a796924f1a..534805eebb 100644 --- a/frappe/www/404.html +++ b/frappe/www/404.html @@ -3,32 +3,22 @@ {%- block title -%}{{_("Not Found")}}{%- endblock -%} {% block page_content %} - + -
    -
    - {{_("Page Missing or Moved")}} + +
    + + -

    {{_("The page you are looking for is missing. This could be because it is moved or there is a typo in the link.")}}

    -
    -

    {{ _("Error Code: {0}").format('404') }}

    - + {% endblock %} \ No newline at end of file From 9ee1613d05ea989607e1d59f0085f7251a33ca58 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 22 Nov 2021 14:36:17 +0530 Subject: [PATCH 50/58] fix: Load assets load required for geolocation control --- .../js/frappe/form/controls/geolocation.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 080a1cbb48..280eac3941 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -3,6 +3,11 @@ frappe.provide('frappe.utils.utils'); frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.form.ControlData { static horizontal = false + async make() { + await frappe.require(this.required_libs); + super.make(); + } + make_wrapper() { // Create the elements for map area super.make_wrapper(); @@ -196,4 +201,17 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f this.editableLayers.removeLayer(l); }); } + + get required_libs() { + return [ + "assets/frappe/js/lib/leaflet/easy-button.css", + "assets/frappe/js/lib/leaflet/L.Control.Locate.css", + "assets/frappe/js/lib/leaflet/leaflet.draw.css", + "assets/frappe/js/lib/leaflet/leaflet.css", + "assets/frappe/js/lib/leaflet/leaflet.js", + "assets/frappe/js/lib/leaflet/easy-button.js", + "assets/frappe/js/lib/leaflet/leaflet.draw.js", + "assets/frappe/js/lib/leaflet/L.Control.Locate.js", + ]; + } }; From 57614e9a6ee366d90fb48c66e7f808c785d113d0 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 22 Nov 2021 18:49:56 +0530 Subject: [PATCH 51/58] fix: Give select permission to 'All' for workflow state (#15044) --- frappe/workflow/doctype/workflow_state/workflow_state.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/workflow/doctype/workflow_state/workflow_state.json b/frappe/workflow/doctype/workflow_state/workflow_state.json index a08f713bb1..be5804f390 100644 --- a/frappe/workflow/doctype/workflow_state/workflow_state.json +++ b/frappe/workflow/doctype/workflow_state/workflow_state.json @@ -112,7 +112,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-02-20 13:33:44.011509", + "modified": "2021-11-22 17:56:40.495232", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow State", @@ -137,6 +137,10 @@ "share": 1, "submit": 0, "write": 1 + }, + { + "role": "All", + "select": 1 } ], "quick_entry": 1, From 6612bb2667064c0d652ab9ca65146909b618909b Mon Sep 17 00:00:00 2001 From: Summayya Date: Mon, 22 Nov 2021 22:16:42 +0530 Subject: [PATCH 52/58] refactor: make user facing strings translatable --- frappe/www/404.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/www/404.html b/frappe/www/404.html index 534805eebb..c03b5d3e96 100644 --- a/frappe/www/404.html +++ b/frappe/www/404.html @@ -12,10 +12,10 @@

    - There's nothing here + {{ _("There's nothing here") }}

    - The page you are looking for have gone missing. + {{ _("The page you are looking for have gone missing.") }}
    From fca5f86a08af426305c1d1dabfe7b7858d87b3a7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 23 Nov 2021 07:10:46 +0530 Subject: [PATCH 53/58] fix: Remove edit icon from quick entry to avoid browser crash --- frappe/public/js/frappe/form/quick_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 2cf2ac38a9..e412b1dec8 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -267,7 +267,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm { render_edit_in_full_page_link() { var me = this; this.dialog.add_custom_action( - `${frappe.utils.icon('edit', 'xs')} ${__("Edit in full page")}`, + `${__("Edit in full page")}`, () => me.open_doc(true) ); } From cb36bfaf045d7f4d18e85df07963567ec9074788 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 23 Nov 2021 08:43:35 +0530 Subject: [PATCH 54/58] style: Fix formatting --- frappe/public/scss/website/error-state.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/website/error-state.scss b/frappe/public/scss/website/error-state.scss index f3fcc140d6..6f88009ecb 100644 --- a/frappe/public/scss/website/error-state.scss +++ b/frappe/public/scss/website/error-state.scss @@ -1,7 +1,7 @@ .error-page { text-align: center; - .img-404{ + .img-404 { width: 40%; margin: var(--margin-2xl) auto; From 5cfe3ad94624fd8d391a3972df12b3516df9db20 Mon Sep 17 00:00:00 2001 From: ritwik Date: Tue, 23 Nov 2021 09:41:06 +0530 Subject: [PATCH 55/58] feat: add no-git option in make-app command and boilerplate generation (#15028) --- frappe/commands/utils.py | 5 +- frappe/tests/test_boilerplate.py | 93 ++++++++++++++++++++++---------- frappe/tests/test_commands.py | 35 +++++++++++- frappe/utils/boilerplate.py | 18 ++++--- 4 files changed, 111 insertions(+), 40 deletions(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index e311b8db6a..41b607b192 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -791,10 +791,11 @@ def request(context, args=None, path=None): @click.command('make-app') @click.argument('destination') @click.argument('app_name') -def make_app(destination, app_name): +@click.option('--no-git', is_flag=True, default=False, help='Do not initialize git repository for the app') +def make_app(destination, app_name, no_git=False): "Creates a boilerplate app" from frappe.utils.boilerplate import make_boilerplate - make_boilerplate(destination, app_name) + make_boilerplate(destination, app_name, no_git=no_git) @click.command('set-config') diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py index 259d5a9194..6a9544b2e9 100644 --- a/frappe/tests/test_boilerplate.py +++ b/frappe/tests/test_boilerplate.py @@ -11,14 +11,7 @@ from frappe.utils.boilerplate import make_boilerplate class TestBoilerPlate(unittest.TestCase): @classmethod - def tearDownClass(cls): - - bench_path = frappe.utils.get_bench_path() - test_app_dir = os.path.join(bench_path, "apps", "test_app") - if os.path.exists(test_app_dir): - shutil.rmtree(test_app_dir) - - def test_create_app(self): + def setUpClass(cls): title = "Test App" description = "This app's description contains 'single quotes' and \"double quotes\"." publisher = "Test Publisher" @@ -27,7 +20,7 @@ class TestBoilerPlate(unittest.TestCase): color = "" app_license = "MIT" - user_input = [ + cls.user_input = [ title, description, publisher, @@ -37,22 +30,21 @@ class TestBoilerPlate(unittest.TestCase): app_license, ] - bench_path = frappe.utils.get_bench_path() - apps_dir = os.path.join(bench_path, "apps") - app_name = "test_app" + cls.bench_path = frappe.utils.get_bench_path() + cls.apps_dir = os.path.join(cls.bench_path, "apps") + cls.app_names = ("test_app", "test_app_no_git") + cls.gitignore_file = ".gitignore" + cls.git_folder = ".git" - with patch("builtins.input", side_effect=user_input): - make_boilerplate(apps_dir, app_name) - - root_paths = [ - app_name, + cls.root_paths = [ "requirements.txt", "README.md", "setup.py", "license.txt", - ".git", + cls.git_folder, + cls.gitignore_file ] - paths_inside_app = [ + cls.paths_inside_app = [ "__init__.py", "hooks.py", "patches.txt", @@ -60,25 +52,68 @@ class TestBoilerPlate(unittest.TestCase): "www", "config", "modules.txt", - "public", - app_name, + "public" ] - new_app_dir = os.path.join(bench_path, apps_dir, app_name) + @classmethod + def tearDownClass(cls): + test_app_dirs = (os.path.join(cls.bench_path, "apps", app_name) for app_name in cls.app_names) + for test_app_dir in test_app_dirs: + if os.path.exists(test_app_dir): + shutil.rmtree(test_app_dir) + def test_create_app(self): + with patch("builtins.input", side_effect=self.user_input): + make_boilerplate(self.apps_dir, self.app_names[0]) + + new_app_dir = os.path.join(self.bench_path, self.apps_dir, self.app_names[0]) + + paths = self.get_paths(new_app_dir, self.app_names[0]) + for path in paths: + self.assertTrue( + os.path.exists(path), + msg=f"{path} should exist in {self.app_names[0]} app" + ) + + self.check_parsable_python_files(new_app_dir) + + def test_create_app_without_git_init(self): + with patch("builtins.input", side_effect=self.user_input): + make_boilerplate(self.apps_dir, self.app_names[1], no_git=True) + + new_app_dir = os.path.join(self.apps_dir, self.app_names[1]) + + paths = self.get_paths(new_app_dir, self.app_names[1]) + for path in paths: + if os.path.basename(path) in (self.git_folder, self.gitignore_file): + self.assertFalse( + os.path.exists(path), + msg=f"{path} shouldn't exist in {self.app_names[1]} app" + ) + else: + self.assertTrue( + os.path.exists(path), + msg=f"{path} should exist in {self.app_names[1]} app" + ) + + self.check_parsable_python_files(new_app_dir) + + def get_paths(self, app_dir, app_name): all_paths = list() - for path in root_paths: - all_paths.append(os.path.join(new_app_dir, path)) + for path in self.root_paths: + all_paths.append(os.path.join(app_dir, path)) - for path in paths_inside_app: - all_paths.append(os.path.join(new_app_dir, app_name, path)) + all_paths.append(os.path.join(app_dir, app_name)) - for path in all_paths: - self.assertTrue(os.path.exists(path), msg=f"{path} should exist in new app") + for path in self.paths_inside_app: + all_paths.append(os.path.join(app_dir, app_name, path)) + return all_paths + + def check_parsable_python_files(self, app_dir): # check if python files are parsable - python_files = glob.glob(new_app_dir + "**/*.py", recursive=True) + python_files = glob.glob(app_dir + "**/*.py", recursive=True) for python_file in python_files: with open(python_file) as p: diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index c048e23949..94389cd7a3 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -5,6 +5,7 @@ import gzip import json import os import shlex +import shutil import subprocess import sys import unittest @@ -102,14 +103,24 @@ def exists_in_backup(doctypes, file): class BaseTestCommands(unittest.TestCase): def execute(self, command, kwargs=None): site = {"site": frappe.local.site} + cmd_input = None if kwargs: + cmd_input = kwargs.get("cmd_input", None) + if cmd_input: + if not isinstance(cmd_input, bytes): + raise Exception( + f"The input should be of type bytes, not {type(cmd_input).__name__}" + ) + + del kwargs["cmd_input"] kwargs.update(site) else: kwargs = site + self.command = " ".join(command.split()).format(**kwargs) print("{0}$ {1}{2}".format(color.silver, self.command, color.nc)) command = shlex.split(self.command) - self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.stdout = clean(self._proc.stdout) self.stderr = clean(self._proc.stderr) self.returncode = clean(self._proc.returncode) @@ -466,6 +477,28 @@ class TestCommands(BaseTestCommands): self.assertEqual(self.returncode, 0) self.assertEqual(check_password('Administrator', 'test2'), 'Administrator') + def test_make_app(self): + user_input = [ + b"Test App", # title + b"This app's description contains 'single quotes' and \"double quotes\".", # description + b"Test Publisher", # publisher + b"example@example.org", # email + b"", # icon + b"", # color + b"MIT" # app_license + ] + app_name = "testapp0" + apps_path = os.path.join(frappe.utils.get_bench_path(), "apps") + test_app_path = os.path.join(apps_path, app_name) + self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)}) + self.assertEqual(self.returncode, 0) + self.assertTrue( + os.path.exists(test_app_path) + ) + + # cleanup + shutil.rmtree(test_app_path) + class RemoveAppUnitTests(unittest.TestCase): def test_delete_modules(self): diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index d0dd1669b4..91f7dbb2f8 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -3,7 +3,7 @@ import frappe, os, re, git from frappe.utils import touch_file, cstr -def make_boilerplate(dest, app_name): +def make_boilerplate(dest, app_name, no_git=False): if not os.path.exists(dest): print("Destination directory does not exist") return @@ -63,9 +63,6 @@ def make_boilerplate(dest, app_name): with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f: f.write(frappe.as_unicode(manifest_template.format(**hooks))) - with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f: - f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name))) - with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f: f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'") @@ -98,11 +95,16 @@ def make_boilerplate(dest, app_name): with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "docs.py"), "w") as f: f.write(frappe.as_unicode(docs_template.format(**hooks))) - # initialize git repository app_directory = os.path.join(dest, hooks.app_name) - app_repo = git.Repo.init(app_directory) - app_repo.git.add(A=True) - app_repo.index.commit("feat: Initialize App") + + if not no_git: + with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f: + f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name))) + + # initialize git repository + app_repo = git.Repo.init(app_directory) + app_repo.git.add(A=True) + app_repo.index.commit("feat: Initialize App") print("'{app}' created at {path}".format(app=app_name, path=app_directory)) From 4a730622fd1de852dd16e8b4cfed43488146d8ed Mon Sep 17 00:00:00 2001 From: Mitul David Date: Tue, 23 Nov 2021 10:39:34 +0530 Subject: [PATCH 56/58] fix: Invalid translation syntax --- frappe/public/js/frappe/file_uploader/FileUploader.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 8d93052cd3..167b4955fa 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -339,15 +339,15 @@ export default { if (!is_correct_type) { console.warn('File skipped because of invalid file type', file); frappe.show_alert({ - message:__(`File "${file.name}" was skipped because of invalid file type`), - indicator:'orange' + message: __('File "{0}" was skipped because of invalid file type', [file.name]), + indicator: 'orange' }); } if (!valid_file_size) { console.warn('File skipped because of invalid file size', file.size, file); frappe.show_alert({ - message:__(`File "${file.name}" was skipped because size exceeds ${max_file_size / (1024 * 1024)} MB`), - indicator:'orange' + message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), + indicator: 'orange' }); } From 98da4b9388f1f3516110b8ac94b2feb760192296 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 23 Nov 2021 10:46:54 +0530 Subject: [PATCH 57/58] chore: Added description on email notification field --- frappe/website/doctype/blog_post/blog_post.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index a7c21b7a48..b01115b818 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -201,6 +201,7 @@ }, { "default": "1", + "description": "Enable or Disable email notification when you get any comment or feedback on your Blog Post.", "fieldname": "enable_email_notification", "fieldtype": "Check", "label": "Enable Email Notification" @@ -213,7 +214,7 @@ "is_published_field": "published", "links": [], "max_attachments": 5, - "modified": "2021-11-16 16:19:18.135696", + "modified": "2021-11-23 10:42:01.759723", "modified_by": "Administrator", "module": "Website", "name": "Blog Post", From 7db461ad6dc9623bc8ebbd295844e1c891aac36b Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 23 Nov 2021 11:03:04 +0530 Subject: [PATCH 58/58] chore: description updated --- frappe/website/doctype/blog_post/blog_post.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index b01115b818..b05293f28b 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -201,7 +201,7 @@ }, { "default": "1", - "description": "Enable or Disable email notification when you get any comment or feedback on your Blog Post.", + "description": "Enable email notification for any comment or feedback on your Blog Post.", "fieldname": "enable_email_notification", "fieldtype": "Check", "label": "Enable Email Notification" @@ -248,4 +248,4 @@ "sort_order": "ASC", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +}