From 2a80bb01ac1a41d5e5fcd70c7a37f62c5c93e8a9 Mon Sep 17 00:00:00 2001 From: "Patrick.St" <72972659+pstuhlmueller@users.noreply.github.com> Date: Tue, 21 Feb 2023 16:13:07 +0100 Subject: [PATCH 001/120] fix: Incorrect use of the Walrus operator Incorrect use of the Walrus operator leads to unintended behavior for if-condition: "None" will be appended to cc. --- frappe/core/doctype/communication/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 7b6427d1c2..7b34208019 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -70,7 +70,7 @@ class CommunicationEmailMixin: if include_sender: cc.append(self.sender_mailid) if is_inbound_mail_communcation: - if (doc_owner := self.get_owner()) not in frappe.STANDARD_USERS: + if (doc_owner := self.get_owner()) and (doc_owner not in frappe.STANDARD_USERS): cc.append(doc_owner) cc = set(cc) - {self.sender_mailid} cc.update(self.get_assignees()) From 841557338b69d455a683ce4f02e8c76a12016b49 Mon Sep 17 00:00:00 2001 From: "Patrick.St" <72972659+pstuhlmueller@users.noreply.github.com> Date: Tue, 21 Feb 2023 16:24:02 +0100 Subject: [PATCH 002/120] fix: sending mails to unintended recipients as cc Security vulnerability: Unintentionally, all incoming emails are sent as CC to all users in a ToDo as "allocated_to" with the status "Open" --- frappe/core/doctype/communication/mixins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 7b34208019..22b7e8a0fc 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -216,7 +216,11 @@ class CommunicationEmailMixin: "reference_name": self.reference_name, "reference_type": self.reference_doctype, } - return ToDo.get_owners(filters) + + if self.reference_doctype == "ToDo" and self.reference_name != None: + return ToDo.get_owners(filters) + else: + return [] @staticmethod def filter_thread_notification_disbled_users(emails): From 735e50e7621bd5465e806f22ad2d849be7f48cce Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 9 Mar 2023 11:10:28 +0100 Subject: [PATCH 003/120] chores: update French translation --- frappe/translations/fr.csv | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frappe/translations/fr.csv b/frappe/translations/fr.csv index d27e2cf49b..b2e419dc2b 100644 --- a/frappe/translations/fr.csv +++ b/frappe/translations/fr.csv @@ -1370,7 +1370,7 @@ Invalid fieldname '{0}' in autoname,Champ invalide '{0}' dans nom automatique, Invalid file path: {0},Chemin de fichier invalide : {0}, Invalid login or password,Identifiant ou mot de passe invalide, Invalid module path,Chemin de module invalide, -Invalid naming series (. missing),Nom de série invalide (. manquant), +Invalid naming series (. missing),Masque de numérotation invalide (. manquant), Invalid payment gateway credentials,Identifiant de la Passerelle de Paiement Invalides, Invalid recipient address,Adresse de destinataire invalide, Invalid {0} condition,Condition {0} invalide, @@ -1601,7 +1601,7 @@ Name of {0} cannot be {1},Nom de {0} ne peut pas être {1}, Names and surnames by themselves are easy to guess.,Les noms et prénoms par eux-mêmes sont faciles à deviner., Naming,Nom, "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
","Options de nommage:
  1. field: [fieldname] - Par champ
  2. naming_series: - En nommant les séries (le champ appelé naming_series doit être présent
  3. Invite - Demander à l'utilisateur un nom
  4. [série] - Série par préfixe (séparé par un point); par exemple PRE. #####
  5. format: EXEMPLE- {MM} morewords {fieldname1} - {fieldname2} - {#####} - Remplace tous les mots accolés (noms de champs, mots de date (JJ, MM, AA), séries) par leur valeur. Des accolades extérieures, des caractères peuvent être utilisés.
", -Naming Series mandatory,Nom de série obligatoire, +Naming Series mandatory,Masque de numérotation obligatoire, Nested set error. Please contact the Administrator.,Erreur d'ensemble imbriqué. Veuillez contacter l'Administrateur., New Activity,Nouvelle activité, New Chat,Nouveau chat, @@ -3428,7 +3428,7 @@ Me,Moi, Mention,Mention, Modules,Modules, Monthly Long,Long mensuel, -Naming Series,Nom de série, +Naming Series,Masque de numérotation, Navigate Home,Naviguer à l'accueil, Navigate list down,Naviguer dans la liste, Navigate list up,Naviguer dans la liste en haut, @@ -4147,7 +4147,7 @@ Collapse,Réduire, {0} is not a valid Name,{0} n'est pas un nom valide, Your system is being updated. Please refresh again after a few moments.,Votre système est en cours de mise à jour. Veuillez actualiser à nouveau après quelques instants., {0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.,{0} {1}: l'enregistrement validé ne peut pas être supprimé. Vous devez d'abord {2} l'annuler {3}., -Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0}, +Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0}, Error has occurred in {0},Une erreur s'est produite dans {0}, Status Updated,Statut mis à jour, You can also copy-paste this {0} to your browser,Vous pouvez également copier-coller ce {0} dans votre navigateur, @@ -4739,3 +4739,8 @@ Auto Reply,Réponse automatique Footer Content,Contenue du pied de page Brand Logo,Logo de la marque Folder Name,Nom du dossier +Document Naming Settings,Masque de numérotation des documents +Setup Series for transactions,Paramétrage des masques de numérotation +Set Naming Series options on your transactions.,Définir les masques de numerotation pour vos documents +Update Series Counter,Mettre à jour le compteur d'un masque de numérotation +Change the starting / current sequence number of an existing series.
\n\nWarning: Incorrectly updating counters can prevent documents from getting created.,Mettre à jour le compteur d'un masque de numérotation
\n\nAttention: Mettre à jour de maniére inconsistance le compteur peux empêcher la création d'un document From ce5ab746338515a72963b542277279b9274cd841 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 9 Mar 2023 11:11:04 +0100 Subject: [PATCH 004/120] chores: update French translation --- frappe/translations/fr.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/translations/fr.csv b/frappe/translations/fr.csv index b2e419dc2b..0f8192b445 100644 --- a/frappe/translations/fr.csv +++ b/frappe/translations/fr.csv @@ -4744,3 +4744,4 @@ Setup Series for transactions,Paramétrage des masques de numérotation Set Naming Series options on your transactions.,Définir les masques de numerotation pour vos documents Update Series Counter,Mettre à jour le compteur d'un masque de numérotation Change the starting / current sequence number of an existing series.
\n\nWarning: Incorrectly updating counters can prevent documents from getting created.,Mettre à jour le compteur d'un masque de numérotation
\n\nAttention: Mettre à jour de maniére inconsistance le compteur peux empêcher la création d'un document +No Results found,Aucun résultat trouvé From 8086baff8d7fdef5075da26eba029ef1bacbdb0e Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 9 Mar 2023 12:20:58 +0100 Subject: [PATCH 005/120] chores: update French translation --- frappe/translations/fr.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/translations/fr.csv b/frappe/translations/fr.csv index 0f8192b445..a33f7adcef 100644 --- a/frappe/translations/fr.csv +++ b/frappe/translations/fr.csv @@ -4745,3 +4745,5 @@ Set Naming Series options on your transactions.,Définir les masques de numerota Update Series Counter,Mettre à jour le compteur d'un masque de numérotation Change the starting / current sequence number of an existing series.
\n\nWarning: Incorrectly updating counters can prevent documents from getting created.,Mettre à jour le compteur d'un masque de numérotation
\n\nAttention: Mettre à jour de maniére inconsistance le compteur peux empêcher la création d'un document No Results found,Aucun résultat trouvé +Shelf Life in Days,Durée de conservation (en jours) +Batch Expiry Date,Date d'expiration du Lot From 2223f97f888a1de89b87a4b1a9a7a7838ef92e7e Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 10 Mar 2023 01:40:22 -0500 Subject: [PATCH 006/120] fix: print preview when no letterhead is selected (#20286) --- frappe/printing/page/print/print.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 54704467c0..f9881fd76e 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -657,7 +657,7 @@ frappe.ui.form.PrintView = class { } get_letterhead() { - return this.letterhead_selector.val(); + return this.letterhead_selector.val() || __("No Letterhead"); } get_no_preview_html() { From 43b5d9532a52d2dbcbb57a1e6703ee24d492b10d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 27 Feb 2023 16:30:31 +0530 Subject: [PATCH 007/120] fix!: create property setters for system generated custom fields --- .../doctype/custom_field/custom_field.js | 21 ++++++++++++++++ .../doctype/customize_form/customize_form.js | 19 +++++++++++--- .../doctype/customize_form/customize_form.py | 25 ++++++++++++------- .../customize_form/test_customize_form.py | 22 ++++++++++++++++ 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index fba19ca45e..be416cb49a 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -25,6 +25,27 @@ frappe.ui.form.on("Custom Field", { frm.toggle_enable("dt", frm.doc.__islocal); frm.trigger("dt"); frm.toggle_reqd("label", !frm.doc.fieldname); + + if (frm.doc.is_system_generated) { + frm.dashboard.add_comment( + __( + "Warning: This field is system generated and may be overwritten by a future update. Modify it using {0} instead.", + [ + frappe.utils.get_form_link( + "Customize Form", + "Customize Form", + true, + __("Customize Form"), + { + doc_type: frm.doc.dt, + } + ), + ] + ), + "yellow", + true + ); + } }, dt: function (frm) { if (!frm.doc.dt) { diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 4ab693b415..dbcc9b17ce 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -251,10 +251,23 @@ frappe.ui.form.on("Customize Form", { // can't delete standard fields frappe.ui.form.on("Customize Form Field", { before_fields_remove: function (frm, doctype, name) { - var row = frappe.get_doc(doctype, name); + let row = frappe.get_doc(doctype, name); + + if (row.is_system_generated) { + frappe.throw( + __( + "Cannot delete system generated field {0}. You can hide it instead.", + [__(row.label) || row.fieldname] + ) + ); + } + if (!(row.is_custom_field || row.__islocal)) { - frappe.msgprint(__("Cannot delete standard field. You can hide it if you want")); - throw "cannot delete standard field"; + frappe.throw( + __("Cannot delete standard field {0}. You can hide it instead.", [ + __(row.label) || row.fieldname, + ]) + ); } }, fields_add: function (frm, cdt, cdn) { diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index bdd18cddfa..42cbf33f4f 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -193,8 +193,9 @@ class CustomizeForm(Document): # docfield for df in self.get("fields"): meta_df = meta.get("fields", {"fieldname": df.fieldname}) - if not meta_df or meta_df[0].get("is_custom_field"): + if not meta_df or not is_standard_or_system_generated_field(meta_df[0]): continue + self.set_property_setters_for_docfield(meta, df, meta_df) # action and links @@ -350,12 +351,14 @@ class CustomizeForm(Document): def update_custom_fields(self): for i, df in enumerate(self.get("fields")): - if df.get("is_custom_field"): - if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}): - self.add_custom_field(df, i) - self.flags.update_db = True - else: - self.update_in_custom_field(df, i) + if is_standard_or_system_generated_field(df): + continue + + if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}): + self.add_custom_field(df, i) + self.flags.update_db = True + else: + self.update_in_custom_field(df, i) self.delete_custom_fields() @@ -380,7 +383,7 @@ class CustomizeForm(Document): def update_in_custom_field(self, df, i): meta = frappe.get_meta(self.doc_type) meta_df = meta.get("fields", {"fieldname": df.fieldname}) - if not (meta_df and meta_df[0].get("is_custom_field")): + if not meta_df or is_standard_or_system_generated_field(meta_df[0]): # not a custom field return @@ -416,7 +419,7 @@ class CustomizeForm(Document): } for fieldname in fields_to_remove: df = meta.get("fields", {"fieldname": fieldname})[0] - if df.get("is_custom_field"): + if not is_standard_or_system_generated_field(df): frappe.delete_doc("Custom Field", df.name) def make_property_setter( @@ -561,6 +564,10 @@ def reset_customization(doctype): frappe.clear_cache(doctype=doctype) +def is_standard_or_system_generated_field(df): + return not df.get("is_custom_field") or df.get("is_system_generated") + + doctype_properties = { "search_fields": "Data", "title_field": "Data", diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 661652c74c..8d98dc4149 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -403,3 +403,25 @@ class TestCustomizeForm(FrappeTestCase): with self.assertRaises(frappe.ValidationError): d.run_method("save_customization") + + def test_system_generated_fields(self): + doctype = "Event" + custom_field_name = "test_custom_field" + + custom_field = frappe.get_doc("Custom Field", {"dt": doctype, "fieldname": custom_field_name}) + custom_field.is_system_generated = 1 + custom_field.save() + + d = self.get_customize_form(doctype) + custom_field = d.getone("fields", {"fieldname": custom_field_name}) + custom_field.description = "Test Description" + d.run_method("save_customization") + + property_setter_filters = { + "doc_type": doctype, + "field_name": custom_field_name, + "property": "description", + } + self.assertEqual( + frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description" + ) From 9a88acfce49ca4937d5785fc618cac3d500dede7 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 27 Feb 2023 16:48:21 +0530 Subject: [PATCH 008/120] fix!: make system generated fields unsortable --- frappe/custom/doctype/customize_form/customize_form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index dbcc9b17ce..40c8dad088 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -89,7 +89,7 @@ frappe.ui.form.on("Customize Form", { setup_sortable: function (frm) { frm.doc.fields.forEach(function (f) { - if (!f.is_custom_field) { + if (!f.is_custom_field || f.is_system_generated) { f._sortable = false; } From 1ed4b2d30e9b0141e6d5417c4e4262541bc412fc Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 1 Mar 2023 15:19:36 +0530 Subject: [PATCH 009/120] style: use `const` instead of `let` --- frappe/custom/doctype/customize_form/customize_form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 40c8dad088..c420dca3e4 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -251,7 +251,7 @@ frappe.ui.form.on("Customize Form", { // can't delete standard fields frappe.ui.form.on("Customize Form Field", { before_fields_remove: function (frm, doctype, name) { - let row = frappe.get_doc(doctype, name); + const row = frappe.get_doc(doctype, name); if (row.is_system_generated) { frappe.throw( From 97ca92e3d18771e0e782c2ccbdfd752a6b106514 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Mar 2023 14:57:15 +0530 Subject: [PATCH 010/120] refactor: change rounding method names (#20299) These are easy to understand. Added third method for corrected banker's rounding. --- cypress/integration/rounding.js | 2 +- frappe/core/doctype/system_settings/system_settings.json | 6 +++--- frappe/public/js/frappe/utils/number_format.js | 6 +++--- frappe/tests/test_utils.py | 6 +++--- frappe/utils/data.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cypress/integration/rounding.js b/cypress/integration/rounding.js index 647b32a6a5..daad2def8f 100644 --- a/cypress/integration/rounding.js +++ b/cypress/integration/rounding.js @@ -8,7 +8,7 @@ context("Rounding behaviour", () => { cy.window() .its("flt") .then((flt) => { - let rounding_method = "Rounding Half Away From Zero"; + let rounding_method = "Commercial Rounding"; expect(flt("0.5", 0, null, rounding_method)).eq(1); expect(flt("0.3", null, null, rounding_method)).eq(0.3); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 2c9e92d943..642e454717 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -523,17 +523,17 @@ "label": "Login with email link expiry (in minutes)" }, { - "default": "Round Half Even", + "default": "Banker's Rounding (legacy)", "fieldname": "rounding_method", "fieldtype": "Select", "label": "Rounding Method", - "options": "Round Half Even\nRounding Half Away From Zero" + "options": "Banker's Rounding (legacy)\nCommercial Rounding" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-03-06 11:31:19.144956", + "modified": "2023-03-10 12:23:45.248125", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 2c457e75fe..52f9ac251b 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -175,11 +175,11 @@ function get_number_format_info(format) { function _round(num, precision, rounding_method) { rounding_method = - rounding_method || frappe.boot.sysdefaults.rounding_method || "Round Half Even"; + rounding_method || frappe.boot.sysdefaults.rounding_method || "Banker's Rounding (legacy)"; let is_negative = num < 0 ? true : false; - if (rounding_method == "Round Half Even") { + if (rounding_method == "Banker's Rounding (legacy)") { var d = cint(precision); var m = Math.pow(10, d); var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors @@ -188,7 +188,7 @@ function _round(num, precision, rounding_method) { var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n); r = d ? r / m : r; return is_negative ? -r : r; - } else if (rounding_method == "Rounding Half Away From Zero") { + } else if (rounding_method == "Commerical Rounding") { if (num == 0) return 0.0; let digits = cint(precision); diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index ce13ee65cd..52e91b9500 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -1007,7 +1007,7 @@ class TestTBSanitization(FrappeTestCase): class TestRounding(FrappeTestCase): - @change_settings("System Settings", {"rounding_method": "Rounding Half Away From Zero"}) + @change_settings("System Settings", {"rounding_method": "Commercial Rounding"}) def test_normal_rounding(self): self.assertEqual(flt("what"), 0) @@ -1041,7 +1041,7 @@ class TestRounding(FrappeTestCase): self.assertEqual(flt(-0.15, 1), -0.2) def test_normal_rounding_as_argument(self): - rounding_method = "Rounding Half Away From Zero" + rounding_method = "Commercial Rounding" self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 1) self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3) @@ -1073,7 +1073,7 @@ class TestRounding(FrappeTestCase): self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.3) self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2) - @change_settings("System Settings", {"rounding_method": "Rounding Half Away From Zero"}) + @change_settings("System Settings", {"rounding_method": "Commercial Rounding"}) @given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4)) def test_normal_rounding_property(self, number, precision): with localcontext() as ctx: diff --git a/frappe/utils/data.py b/frappe/utils/data.py index eeae737144..8a6037d718 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1055,12 +1055,12 @@ def rounded(num, precision=0, rounding_method=None): precision = cint(precision) rounding_method = ( - rounding_method or frappe.get_system_settings("rounding_method") or "Round Half Even" + rounding_method or frappe.get_system_settings("rounding_method") or "Banker's Rounding (legacy)" ) - if rounding_method == "Round Half Even": + if rounding_method == "Banker's Rounding (legacy)": return _round_half_even(num, precision) - elif rounding_method == "Rounding Half Away From Zero": + elif rounding_method == "Commercial Rounding": return _round_away_from_zero(num, precision) else: frappe.throw( From 90be13e7ee7e7abb5a4772b4dfdd3b35fbfc939a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 13 Mar 2023 11:06:15 +0530 Subject: [PATCH 011/120] fix: dont respond to login api call if email login is disabled (#20311) --- frappe/www/login.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/www/login.py b/frappe/www/login.py index 8529b03bf6..3cd9edf7ce 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -121,6 +121,9 @@ def login_via_token(login_token: str): @rate_limit(limit=5, seconds=60 * 60) def send_login_link(email: str): + if not frappe.get_system_settings("login_with_email_link"): + return + expiry = frappe.get_system_settings("login_with_email_link_expiry") or 10 link = _generate_temporary_login_link(email, expiry) From 032889f9137eb05d6d9d59654bba644e2629133a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 13 Mar 2023 12:28:20 +0530 Subject: [PATCH 012/120] chore: typo --- frappe/public/js/frappe/utils/number_format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 52f9ac251b..2c520805f2 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -188,7 +188,7 @@ function _round(num, precision, rounding_method) { var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n); r = d ? r / m : r; return is_negative ? -r : r; - } else if (rounding_method == "Commerical Rounding") { + } else if (rounding_method == "Commercial Rounding") { if (num == 0) return 0.0; let digits = cint(precision); From 139d4a87b464071cf9d97df3d2b9cd877ee56887 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Mar 2023 12:26:51 +0530 Subject: [PATCH 013/120] fix: corrected banker's rounding closes https://github.com/frappe/frappe/issues/19570 --- cypress/integration/rounding.js | 62 +++++++++++++++- .../system_settings/system_settings.json | 2 +- .../public/js/frappe/utils/number_format.js | 19 +++++ frappe/tests/test_utils.py | 71 +++++++++++++++++++ frappe/utils/data.py | 22 +++++- 5 files changed, 172 insertions(+), 4 deletions(-) diff --git a/cypress/integration/rounding.js b/cypress/integration/rounding.js index daad2def8f..1a9cfa685b 100644 --- a/cypress/integration/rounding.js +++ b/cypress/integration/rounding.js @@ -4,7 +4,7 @@ context("Rounding behaviour", () => { cy.visit("/app/"); }); - it("Rounds floats accurately", () => { + it("Commercial Rounding", () => { cy.window() .its("flt") .then((flt) => { @@ -41,4 +41,64 @@ context("Rounding behaviour", () => { expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2); }); }); + + it("Banker's Rounding", () => { + cy.window() + .its("flt") + .then((flt) => { + let rounding_method = "Banker's Rounding"; + + expect(flt("0.5", 0, null, rounding_method)).eq(0); + expect(flt("0.3", null, rounding_method)).eq(0.3); + + expect(flt("1.5", 0, null, rounding_method)).eq(2); + + // positive rounding to integers + expect(flt(0.4, 0, null, rounding_method)).eq(0); + expect(flt(0.5, 0, null, rounding_method)).eq(0); + expect(flt(1.455, 0, null, rounding_method)).eq(1); + expect(flt(1.5, 0, null, rounding_method)).eq(2); + + // negative rounding to integers + expect(flt(-0.5, 0, null, rounding_method)).eq(0); + expect(flt(-1.5, 0, null, rounding_method)).eq(-2); + + // negative precision i.e. round to nearest 10th + expect(flt(123, -1, null, rounding_method)).eq(120); + expect(flt(125, -1, null, rounding_method)).eq(120); + expect(flt(134.45, -1, null, rounding_method)).eq(130); + expect(flt(135, -1, null, rounding_method)).eq(140); + + // positive multiple digit rounding + expect(flt(1.25, 1, null, rounding_method)).eq(1.2); + expect(flt(0.15, 1, null, rounding_method)).eq(0.2); + expect(flt(2.675, 2, null, rounding_method)).eq(2.68); + expect(flt(-2.675, 2, null, rounding_method)).eq(-2.68); + + // negative multiple digit rounding + expect(flt(-1.25, 1, null, rounding_method)).eq(-1.2); + expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2); + + // Nearest number and not even (the default behaviour) + expect(flt(0.5, 0, null, rounding_method)).eq(0); + expect(flt(1.5, 0, null, rounding_method)).eq(2); + expect(flt(2.5, 0, null, rounding_method)).eq(2); + expect(flt(3.5, 0, null, rounding_method)).eq(4); + + expect(flt(0.05, 1, null, rounding_method)).eq(0.0); + expect(flt(1.15, 1, null, rounding_method)).eq(1.2); + expect(flt(2.25, 1, null, rounding_method)).eq(2.2); + expect(flt(3.35, 1, null, rounding_method)).eq(3.4); + + expect(flt(-0.5, 0, null, rounding_method)).eq(0); + expect(flt(-1.5, 0, null, rounding_method)).eq(-2); + expect(flt(-2.5, 0, null, rounding_method)).eq(-2); + expect(flt(-3.5, 0, null, rounding_method)).eq(-4); + + expect(flt(-0.05, 1, null, rounding_method)).eq(0.0); + expect(flt(-1.15, 1, null, rounding_method)).eq(-1.2); + expect(flt(-2.25, 1, null, rounding_method)).eq(-2.2); + expect(flt(-3.35, 1, null, rounding_method)).eq(-3.4); + }); + }); }); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 642e454717..8ebcb493de 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -527,7 +527,7 @@ "fieldname": "rounding_method", "fieldtype": "Select", "label": "Rounding Method", - "options": "Banker's Rounding (legacy)\nCommercial Rounding" + "options": "Banker's Rounding (legacy)\nBanker's Rounding\nCommercial Rounding" } ], "icon": "fa fa-cog", diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 2c520805f2..179c186908 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -188,6 +188,25 @@ function _round(num, precision, rounding_method) { var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n); r = d ? r / m : r; return is_negative ? -r : r; + } else if (rounding_method == "Banker's Rounding") { + precision = cint(precision); + + let multiplier = Math.pow(10, precision); + num = Math.abs(num) * multiplier; + + let floor_num = Math.floor(num); + let decimal_part = num - floor_num; + + // For explanation of this method read python flt implementation notes. + let epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0); + + if (Math.abs(decimal_part - 0.5) < epsilon) { + num = floor_num % 2 == 0 ? floor_num : floor_num + 1; + } else { + num = Math.round(num); + } + num = num / multiplier; + return is_negative ? -num : num; } else if (rounding_method == "Commercial Rounding") { if (num == 0) return 0.0; diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 52e91b9500..b897c96133 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -1073,9 +1073,80 @@ class TestRounding(FrappeTestCase): self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.3) self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2) + # Nearest number and not even (the default behaviour) + self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 1) + self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2) + self.assertEqual(flt(2.5, 0, rounding_method=rounding_method), 3) + self.assertEqual(flt(3.5, 0, rounding_method=rounding_method), 4) + + self.assertEqual(flt(0.05, 1, rounding_method=rounding_method), 0.1) + self.assertEqual(flt(1.15, 1, rounding_method=rounding_method), 1.2) + self.assertEqual(flt(2.25, 1, rounding_method=rounding_method), 2.3) + self.assertEqual(flt(3.35, 1, rounding_method=rounding_method), 3.4) + @change_settings("System Settings", {"rounding_method": "Commercial Rounding"}) @given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4)) def test_normal_rounding_property(self, number, precision): with localcontext() as ctx: ctx.rounding = ROUND_HALF_UP self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision)) + + def test_bankers_rounding(self): + rounding_method = "Banker's Rounding" + + self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 0) + self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3) + + self.assertEqual(flt("1.5", 0, rounding_method=rounding_method), 2) + + # positive rounding to integers + self.assertEqual(flt(0.4, 0, rounding_method=rounding_method), 0) + self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 0) + self.assertEqual(flt(1.455, 0, rounding_method=rounding_method), 1) + self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2) + + # negative rounding to integers + self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), 0) + self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2) + + # negative precision i.e. round to nearest 10th + self.assertEqual(flt(123, -1, rounding_method=rounding_method), 120) + self.assertEqual(flt(125, -1, rounding_method=rounding_method), 120) + self.assertEqual(flt(134.45, -1, rounding_method=rounding_method), 130) + self.assertEqual(flt(135, -1, rounding_method=rounding_method), 140) + + # positive multiple digit rounding + self.assertEqual(flt(1.25, 1, rounding_method=rounding_method), 1.2) + self.assertEqual(flt(0.15, 1, rounding_method=rounding_method), 0.2) + self.assertEqual(flt(2.675, 2, rounding_method=rounding_method), 2.68) + self.assertEqual(flt(-2.675, 2, rounding_method=rounding_method), -2.68) + + # negative multiple digit rounding + self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.2) + self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2) + + # Nearest number and not even (the default behaviour) + self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 0) + self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2) + self.assertEqual(flt(2.5, 0, rounding_method=rounding_method), 2) + self.assertEqual(flt(3.5, 0, rounding_method=rounding_method), 4) + + self.assertEqual(flt(0.05, 1, rounding_method=rounding_method), 0.0) + self.assertEqual(flt(1.15, 1, rounding_method=rounding_method), 1.2) + self.assertEqual(flt(2.25, 1, rounding_method=rounding_method), 2.2) + self.assertEqual(flt(3.35, 1, rounding_method=rounding_method), 3.4) + + self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), 0) + self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2) + self.assertEqual(flt(-2.5, 0, rounding_method=rounding_method), -2) + self.assertEqual(flt(-3.5, 0, rounding_method=rounding_method), -4) + + self.assertEqual(flt(-0.05, 1, rounding_method=rounding_method), 0.0) + self.assertEqual(flt(-1.15, 1, rounding_method=rounding_method), -1.2) + self.assertEqual(flt(-2.25, 1, rounding_method=rounding_method), -2.2) + self.assertEqual(flt(-3.35, 1, rounding_method=rounding_method), -3.4) + + @change_settings("System Settings", {"rounding_method": "Banker's Rounding"}) + @given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4)) + def test_bankers_rounding_property(self, number, precision): + self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision)) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 8a6037d718..26fb50c31b 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1059,7 +1059,9 @@ def rounded(num, precision=0, rounding_method=None): ) if rounding_method == "Banker's Rounding (legacy)": - return _round_half_even(num, precision) + return _bankers_rounding_legacy(num, precision) + elif rounding_method == "Banker's Rounding": + return _bankers_rounding(num, precision) elif rounding_method == "Commercial Rounding": return _round_away_from_zero(num, precision) else: @@ -1069,7 +1071,7 @@ def rounded(num, precision=0, rounding_method=None): ) -def _round_half_even(num, precision): +def _bankers_rounding_legacy(num, precision): # avoid rounding errors multiplier = 10**precision num = round(num * multiplier if precision else num, 8) @@ -1114,6 +1116,22 @@ def _round_away_from_zero(num, precision): return round(num + math.copysign(epsilon, num), precision) +def _bankers_rounding(num, precision): + multiplier = 10**precision + num = round(num * multiplier, 12) + + floor_num = math.floor(num) + decimal_part = num - floor_num + + epsilon = 2.0 ** (math.log(abs(num), 2) - 52.0) + if abs(decimal_part - 0.5) < epsilon: + num = floor_num if (floor_num % 2 == 0) else floor_num + 1 + else: + num = round(num) + + return num / multiplier + + def remainder(numerator: NumericType, denominator: NumericType, precision: int = 2) -> NumericType: precision = cint(precision) multiplier = 10**precision From 709edf1f559ba30ffe8015542eb7b4de6c6e7ceb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 13 Mar 2023 13:00:50 +0530 Subject: [PATCH 014/120] fix: Make corrected bankers rounding default method --- .../doctype/system_settings/system_settings.js | 15 +++++++++++++++ frappe/desk/page/setup_wizard/setup_wizard.py | 1 + frappe/tests/test_utils.py | 3 +++ 3 files changed, 19 insertions(+) diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index 1d5ba7ddb0..bf8988d64c 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -16,6 +16,8 @@ frappe.ui.form.on("System Settings", { } }, }); + + frm.trigger("set_rounding_method_options"); }, enable_password_policy: function (frm) { if (frm.doc.enable_password_policy == 0) { @@ -56,4 +58,17 @@ frappe.ui.form.on("System Settings", { } ); }, + + set_rounding_method_options: function (frm) { + if (frm.doc.rounding_method != "Banker's Rounding (legacy)") { + let field = frm.fields_dict.rounding_method; + + field.df.options = field.df.options + .split("\n") + .filter((o) => o != "Banker's Rounding (legacy)") + .join("\n"); + + field.refresh(); + } + }, }); diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 4d4d76207a..cdb25b81ba 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -165,6 +165,7 @@ def update_system_settings(args): "language": get_language_code(args.get("language")) or "en", "time_zone": args.get("timezone"), "float_precision": 3, + "rounding_method": "Banker's Rounding", "date_format": frappe.db.get_value("Country", args.get("country"), "date_format"), "time_format": frappe.db.get_value("Country", args.get("country"), "time_format"), "number_format": number_format, diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index b897c96133..ffc8e80acf 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -1150,3 +1150,6 @@ class TestRounding(FrappeTestCase): @given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4)) def test_bankers_rounding_property(self, number, precision): self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision)) + + def test_default_rounding(self): + self.assertEqual(frappe.get_system_settings("rounding_method"), "Banker's Rounding") From dd2ac72a9a799139befb4025c458c181e911bd03 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 13 Mar 2023 14:14:17 +0530 Subject: [PATCH 015/120] fix: skip 0 for rounding --- frappe/public/js/frappe/utils/number_format.js | 1 + frappe/tests/test_utils.py | 2 ++ frappe/utils/data.py | 3 +++ 3 files changed, 6 insertions(+) diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 179c186908..ef4c7366f7 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -189,6 +189,7 @@ function _round(num, precision, rounding_method) { r = d ? r / m : r; return is_negative ? -r : r; } else if (rounding_method == "Banker's Rounding") { + if (num == 0) return 0.0; precision = cint(precision); let multiplier = Math.pow(10, precision); diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index ffc8e80acf..dce2a159ac 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -63,6 +63,7 @@ from frappe.utils.data import ( now_datetime, nowtime, pretty_date, + rounded, to_timedelta, validate_python_code, ) @@ -1094,6 +1095,7 @@ class TestRounding(FrappeTestCase): def test_bankers_rounding(self): rounding_method = "Banker's Rounding" + self.assertEqual(rounded(0, 0, rounding_method=rounding_method), 0) self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 0) self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 26fb50c31b..d76d97f7e0 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1117,6 +1117,9 @@ def _round_away_from_zero(num, precision): def _bankers_rounding(num, precision): + if num == 0: + return 0.0 + multiplier = 10**precision num = round(num * multiplier, 12) From 41685ff014e6caf04ce11a3454bde3df98a918a8 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 13 Mar 2023 15:20:57 +0530 Subject: [PATCH 016/120] fix: dont use get_datetime for enddate when startdate is in future for google calendar event (#20309) --- .../integrations/doctype/google_calendar/google_calendar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index a663c9c593..65a4a2bccd 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -403,7 +403,7 @@ def insert_event_in_google_calendar(doc, method=None): event = {"summary": doc.subject, "description": doc.description, "google_calendar_event": 1} event.update( format_date_according_to_google_calendar( - doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) + doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) if doc.ends_on else None ) ) @@ -484,7 +484,7 @@ def update_event_in_google_calendar(doc, method=None): ) event.update( format_date_according_to_google_calendar( - doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) + doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) if doc.ends_on else None ) ) From 985a8ca18a8be1e18c745b6b3af3bbcdd891eb0a Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 13 Mar 2023 21:05:48 +0530 Subject: [PATCH 017/120] fix: proper errror translation for finding outgoing email account (#20316) --- frappe/email/doctype/email_account/email_account.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index aa0935e028..2e5dbe2e24 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -19,7 +19,6 @@ from frappe.email.utils import get_port from frappe.model.document import Document from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.utils.error import raise_error_on_no_output from frappe.utils.jinja import render_template from frappe.utils.user import get_system_managers @@ -301,11 +300,6 @@ class EmailAccount(Document): return cls.from_record({"sender": "notifications@example.com"}) @classmethod - @raise_error_on_no_output( - keep_quiet=lambda: not cint(frappe.get_system_settings("setup_complete")), - error_message=_("Please setup default Email Account from Setup > Email > Email Account"), - error_type=frappe.OutgoingEmailError, - ) # noqa @cache_email_account("outgoing_email_account") def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False): """Find the outgoing Email account to use. @@ -329,6 +323,12 @@ class EmailAccount(Document): if doc: return {"default": doc} + if _raise_error: + frappe.throw( + _("Please setup default Email Account from Setup > Email > Email Account"), + frappe.OutgoingEmailError, + ) + @classmethod def find_default_outgoing(cls): """Find default outgoing account.""" From 5538d7fbbeb1e0a69baa737e953fd38951fd9b46 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Mar 2023 07:07:22 +0100 Subject: [PATCH 018/120] chore: translate read only alert to german (#20324) [skip ci] --- frappe/translations/de.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index e6a8fa4a35..b3f84cb072 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -4832,3 +4832,4 @@ CSV Preview,Vorschau, Non-numeric,Nicht-numerische, Minimal,Minimal, This value is fetched from {0}'s {1} field,Dieser Wert ergibt sich aus dem Feld {1} von {0}, +This form is not editable due to a Workflow.,Dieses Formular kann in diesem Workflow-Status nicht bearbeitet werden., From da086a44107b6ccff769c4b9fb66c60dfcf1787e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Mar 2023 12:43:41 +0530 Subject: [PATCH 019/120] feat: allow configuring ttl for RQ job retention (#20331) Some might want to keep them around for long, others might wanna get rid of it to free up memory. Hardcoded defaults dont work for everyone hence make it configurable. `no-docs` [skip ci] --- frappe/core/doctype/rq_job/test_rq_job.py | 6 ++++++ frappe/database/database.py | 4 ++-- frappe/utils/background_jobs.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index 2fbefda056..3b8e2d5cc3 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -44,6 +44,12 @@ class TestRQJob(FrappeTestCase): ) self.check_status(job, "finished") + def test_configurable_ttl(self): + frappe.conf.rq_job_failure_ttl = 600 + job = frappe.enqueue(method=self.BG_JOB, queue="short") + + self.assertEqual(job.failure_ttl, 600) + def test_func_obj_serialization(self): job = frappe.enqueue(method=test_func, queue="short") rq_job = frappe.get_doc("RQ Job", job.id) diff --git a/frappe/database/database.py b/frappe/database/database.py index 7e0cb83454..c51a8f10a7 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1300,8 +1300,8 @@ def enqueue_jobs_after_commit(): execute_job, timeout=job.get("timeout"), kwargs=job.get("queue_args"), - failure_ttl=RQ_JOB_FAILURE_TTL, - result_ttl=RQ_RESULTS_TTL, + failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL, + result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL, ) frappe.flags.enqueue_after_commit = [] diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index fed822700c..29098dce3b 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -126,8 +126,8 @@ def enqueue( timeout=timeout, kwargs=queue_args, at_front=at_front, - failure_ttl=RQ_JOB_FAILURE_TTL, - result_ttl=RQ_RESULTS_TTL, + failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL, + result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL, ) From e429101370b85a7457f79f237c477df1a2a33896 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 14 Mar 2023 15:13:35 +0530 Subject: [PATCH 020/120] fix(print): Language set in document should have higher precedence --- frappe/printing/page/print/print.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index f9881fd76e..8e5e165c78 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -347,7 +347,7 @@ frappe.ui.form.PrintView = class { set_default_print_language() { let print_format = this.get_print_format(); this.lang_code = - print_format.default_print_language || this.frm.doc.language || frappe.boot.lang; + this.frm.doc.language || print_format.default_print_language || frappe.boot.lang; this.language_selector.val(this.lang_code); } From 9430b6a8c2c6e75cb36c00bd87342cf05d281e84 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Mar 2023 17:27:30 +0530 Subject: [PATCH 021/120] fix: Permission error while processing role based notifications (#20315) --- frappe/core/doctype/role/role.py | 3 ++- frappe/email/doctype/notification/notification.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 97a0e9b581..31b82501cb 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -64,13 +64,14 @@ class Role(Document): user.save() -def get_info_based_on_role(role, field="email"): +def get_info_based_on_role(role, field="email", ignore_permissions=False): """Get information of all users that have been assigned this role""" users = frappe.get_list( "Has Role", filters={"role": role, "parenttype": "User"}, parent_doctype="User", fields=["parent as user_name"], + ignore_permissions=ignore_permissions, ) return get_user_info(users, field) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index d5071e23a0..2efbf597ec 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -298,7 +298,7 @@ def get_context(context): # For sending emails to specified role if recipient.receiver_by_role: - emails = get_info_based_on_role(recipient.receiver_by_role, "email") + emails = get_info_based_on_role(recipient.receiver_by_role, "email", ignore_permissions=True) for email in emails: recipients = recipients + email.split("\n") From 1f01eccb9b8b156d4e17f9e0ca541bbc415c7215 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Tue, 14 Mar 2023 21:21:13 +0100 Subject: [PATCH 022/120] chore: update de translations (#20341) added translations for new fields (v14) --- frappe/translations/de.csv | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index b3f84cb072..a6aef52b1c 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -1611,6 +1611,7 @@ Name of the new Print Format,Name des neuen Druckformats, Name of {0} cannot be {1},Name von {0} kann nicht {1} sein, Names and surnames by themselves are easy to guess.,Namen und Vornamen von Ihnen selbst sind leicht zu erraten., Naming,Bezeichnung, +Naming Rule, Benennungsregel, "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
","Namensoptionen:
  1. Feld: [Feldname] - Nach Feld
  2. naming_series: - Nach der Namensreihe (das Feld naming_series muss vorhanden sein)
  3. Eingabeaufforderung - Benutzer nach einem Namen fragen
  4. [Serie] - Reihe nach Präfix (getrennt durch einen Punkt); zum Beispiel PRE. #####
  5. Format: BEISPIEL- {MM} morewords {Feldname1} - {Feldname2} - {#####} - Ersetzt alle verspannten Wörter (Feldnamen, Datumsworte (DD, MM, YY), Serien) durch ihren Wert. Außerhalb von Klammern können beliebige Zeichen verwendet werden.
", Naming Series mandatory,Nummernkreis zwingend erforderlich, Nested set error. Please contact the Administrator.,Schachtelfehler. Bitte den Administrator kontaktieren., @@ -2311,6 +2312,7 @@ Show Report,Bericht zeigen, Show Section Headings,Zeige Abschnittsüberschriften, Show Sidebar,anzeigen Sidebar, Show Title,Bezeichnung anzeigen, +Show Title in Link Fields,Bezeichnung in Verknüpfungsfeld anzeigen, Show Totals,Summen anzeigen, Show Weekends,Wochenenden anzeigen, Show all Versions,Alle Versionen, @@ -2574,7 +2576,7 @@ Track Changes,Änderungen verfolgen, Track Email Status,E-Mail-Status verfolgen, Track Field,Track Field, Track Seen,Track gesehen, -Track Views,Trackansichten, +Track Views,Ansichten verfolgen, "Track if your email has been opened by the recipient.\n
\nNote: If you're sending to multiple recipients, even if 1 recipient reads the email, it'll be considered ""Opened""","Verfolgen Sie, ob Ihre E-Mail vom Empfänger geöffnet wurde.
Hinweis: Wenn Sie an mehrere Empfänger senden, auch wenn 1 Empfänger die E-Mail liest, wird sie als "Geöffnet" betrachtet.", Track milestones for any document,Verfolgen Sie Meilensteine für jedes Dokument, Transaction Hash,Transaktions-Hash, @@ -2584,6 +2586,7 @@ Transition Rules,Übergangsbestimmungen, Transitions,Übergänge, Translatable,Übersetzbar, Translate {0},Übersetzen {0}, +Translate Link Fields,Verknünpfungsfelder übersetzen, Translated Text,Übersetzter Text, Translation,Übersetzung, Translations,Übersetzungen, @@ -3254,9 +3257,11 @@ Cron,Cron, Cron Format,Cron Format, Daily Events should finish on the Same Day.,Tägliche Ereignisse sollten am selben Tag enden., Daily Long,Täglich lang, +Default {0},Standard {0}, +Default Email Template, Standard E-Mailvorlage, Default Role on Creation,Standardrolle bei der Erstellung, Default Theme,Standardthema, -Default {0},Standard {0}, +Default View,Standardansicht, Delete All,Alles löschen, Do you want to cancel all linked documents?,Möchten Sie alle verknüpften Dokumente stornieren?, DocType Action,DocType-Aktion, From 2c16ad2bfb3115797fcf19582515e6eb62ea9ae1 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 15 Mar 2023 13:41:46 +0530 Subject: [PATCH 023/120] fix: sidebar becomes unhidden while removing skeleton --- frappe/public/js/frappe/views/workspace/workspace.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index beb799124c..65d0b583ea 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -1445,15 +1445,15 @@ frappe.views.Workspace = class Workspace { } create_sidebar_skeleton() { - if (this.sidebar.find(".workspace-sidebar-skeleton").length) return; + if ($(".workspace-sidebar-skeleton").length) return; - this.sidebar.prepend(frappe.render_template("workspace_sidebar_loading_skeleton")); - this.sidebar.find(".standard-sidebar-section").addClass("hidden"); + $(frappe.render_template("workspace_sidebar_loading_skeleton")).insertBefore(this.sidebar); + this.sidebar.addClass("hidden"); } remove_sidebar_skeleton() { - this.sidebar.find(".standard-sidebar-section").removeClass("hidden"); - this.sidebar.find(".workspace-sidebar-skeleton").remove(); + this.sidebar.removeClass("hidden"); + $(".workspace-sidebar-skeleton").remove(); } register_awesomebar_shortcut() { From 20eebe7340d733a815df03994ae1c30b4c0d4982 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 15 Mar 2023 13:43:16 +0530 Subject: [PATCH 024/120] fix: hide My Workspace sidebar section if empty in edit mode --- frappe/public/js/frappe/views/workspace/workspace.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 65d0b583ea..af02629038 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -157,7 +157,10 @@ frappe.views.Workspace = class Workspace { sidebar_section.addClass("hidden"); } - if (sidebar_section.find("> [item-is-hidden='0']").length == 0) { + if ( + sidebar_section.find("sidebar-item-container").length && + sidebar_section.find("> [item-is-hidden='0']").length == 0 + ) { sidebar_section.addClass("hidden show-in-edit-mode"); } } From 09ea38e96fb0d9fe98e89933785017219d28656e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 15 Mar 2023 14:14:41 +0530 Subject: [PATCH 025/120] fix: Set link title in PDF Co-authored-by: Saqib Ansari --- frappe/www/printview.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/www/printview.py b/frappe/www/printview.py index f67aa9eee1..9fdc77a2ba 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -40,6 +40,8 @@ def get_context(context): else: doc = frappe.get_doc(frappe.form_dict.doctype, frappe.form_dict.name) + set_link_titles(doc) + settings = frappe.parse_json(frappe.form_dict.settings) letterhead = frappe.form_dict.letterhead or None From 56fb0a4f93be977c3bdd70fd886e5f6efb45276c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 15 Mar 2023 15:12:13 +0530 Subject: [PATCH 026/120] fix: treat Phone as Data on list view closes https://github.com/frappe/frappe/issues/20290 --- 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 2bf738d28b..600db57dd1 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -773,6 +773,7 @@ class FilterArea { "HTML Editor", "Data", "Code", + "Phone", "Read Only", ].includes(fieldtype) ) { From 8dde2cd43ccadbb1e1ad821f80c4f1e7d39b61cd Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 16 Mar 2023 09:45:43 +0530 Subject: [PATCH 027/120] fix: Clear dasboard comment to avoid duplicate - on save --- frappe/core/doctype/doctype/doctype.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index d92277152c..37d80571d2 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -48,6 +48,7 @@ frappe.ui.form.on("DocType", { true ); } else if (frappe.boot.developer_mode) { + frm.dashboard.clear_comment(); let msg = __( "This site is running in developer mode. Any change made here will be updated in code." ); From 10a5861f11cc2da8097bb6b3f966194fa80ab47f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 16 Mar 2023 09:47:53 +0530 Subject: [PATCH 028/120] fix: Fill partially entered data to quick entry - if the autoname is "prompt" the auto fill was not working Co-authored-by: Rushabh Mehta --- frappe/public/js/frappe/model/create_new.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index ebe565f6a4..523cb3b7a6 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -48,6 +48,8 @@ $.extend(frappe.model, { // set title field / name as name if (meta.autoname && meta.autoname.indexOf("field:") !== -1) { doc[meta.autoname.substr(6)] = frappe.route_options.name_field; + } else if (meta.autoname && meta.autoname === "prompt") { + doc.__newname = frappe.route_options.name_field; } else if (meta.title_field) { doc[meta.title_field] = frappe.route_options.name_field; } From 8aac4aae759356b02bda099288c749dc31ea69f4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 16 Mar 2023 10:10:52 +0530 Subject: [PATCH 029/120] fix(grid): Show filter row if some filters are already applied --- frappe/public/js/frappe/form/grid_row.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index f0795f1213..27ca3dba0c 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -40,7 +40,7 @@ export default class GridRow { render_row = this.render_row(); } - if (!this.render_row) return; + if (!render_row) return; this.set_data(); this.wrapper.appendTo(this.parent); @@ -762,7 +762,8 @@ export default class GridRow { show_search_row() { // show or remove search columns based on grid rows - this.show_search = this.show_search && this.grid?.data?.length >= 20; + this.show_search = + this.show_search && (this.grid?.data?.length >= 20 || this.grid.filter_applied); !this.show_search && this.wrapper.remove(); return this.show_search; } From 6b79a9756697e2c56a18a28c8540ba6c77f198b7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 16 Mar 2023 11:03:45 +0530 Subject: [PATCH 030/120] fix(UX): clear comment to avoid duplicates --- frappe/core/doctype/doctype/doctype.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index 37d80571d2..a8eed4270d 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -42,6 +42,7 @@ frappe.ui.form.on("DocType", { if (!frappe.boot.developer_mode && !frm.doc.custom) { // make the document read-only frm.set_read_only(); + frm.dashboard.clear_comment(); frm.dashboard.add_comment( __("DocTypes can not be modified, please use {0} instead", [customize_form_link]), "blue", From c542882bf26d529457777e0aa1f3fc8a009733e0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 16 Mar 2023 11:40:47 +0530 Subject: [PATCH 031/120] fix: Remove lang selector in communication to avoid confusion since the feature is not functional --- .../public/js/frappe/views/communication.js | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 713afd0895..7230de9cf5 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -130,11 +130,6 @@ frappe.views.CommunicationComposer = class { fieldtype: "Select", fieldname: "select_print_format", }, - { - label: __("Select Languages"), - fieldtype: "Select", - fieldname: "language_sel", - }, { fieldtype: "Column Break" }, { label: __("Select Attachments"), @@ -183,7 +178,6 @@ frappe.views.CommunicationComposer = class { prepare() { this.setup_multiselect_queries(); this.setup_subject_and_recipients(); - this.setup_print_language(); this.setup_print(); this.setup_attach(); this.setup_email(); @@ -295,7 +289,6 @@ frappe.views.CommunicationComposer = class { args: { template_name: email_template, doc: me.doc, - _lang: me.dialog.get_value("language_sel"), }, callback(r) { prepend_reply(r.message); @@ -403,29 +396,6 @@ frappe.views.CommunicationComposer = class { } } - setup_print_language() { - const fields = this.dialog.fields_dict; - - //Load default print language from doctype - this.lang_code = - this.doc.language || - this.get_print_format().default_print_language || - frappe.boot.lang; - - //On selection of language retrieve language code - const me = this; - $(fields.language_sel.input).change(function () { - me.lang_code = this.value; - }); - - // Load all languages in the select field language_sel - $(fields.language_sel.input).empty().add_options(frappe.get_languages()); - - if (this.lang_code) { - $(fields.language_sel.input).val(this.lang_code); - } - } - setup_print() { // print formats const fields = this.dialog.fields_dict; @@ -676,7 +646,6 @@ frappe.views.CommunicationComposer = class { sender_full_name: form_values.sender ? frappe.user.full_name() : undefined, email_template: form_values.email_template, attachments: selected_attachments, - _lang: me.lang_code, read_receipt: form_values.send_read_receipt, print_letterhead: me.is_print_letterhead_checked(), }, From fdcdb61a2dc5ae8306fc67e406da8d1c6b9cbd80 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 16 Mar 2023 14:01:25 +0530 Subject: [PATCH 032/120] fix: email linking and message_id indexing (#20356) * fix: find communication regardless of system reply * perf: convert and index message_id --- .../doctype/communication/communication.json | 4 ++-- .../doctype/communication/communication.py | 1 + .../doctype/email_queue/email_queue.json | 7 +++--- .../email/doctype/email_queue/email_queue.py | 2 ++ frappe/email/receive.py | 22 +++++++++---------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 293a6b2c87..e5f090e2f7 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -318,7 +318,7 @@ }, { "fieldname": "message_id", - "fieldtype": "Data", + "fieldtype": "Small Text", "ignore_xss_filter": 1, "label": "Message ID", "length": 995, @@ -395,7 +395,7 @@ "icon": "fa fa-comment", "idx": 1, "links": [], - "modified": "2022-05-09 00:13:45.310564", + "modified": "2023-03-16 12:04:18.113817", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index adeac35204..067cba59b2 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -397,6 +397,7 @@ def on_doctype_update(): """Add indexes in `tabCommunication`""" frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) frappe.db.add_index("Communication", ["status", "communication_type"]) + frappe.db.add_index("Communication", ["message_id(140)"]) def has_permission(doc, ptype, user): diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json index c9ec374687..ac8d656678 100644 --- a/frappe/email/doctype/email_queue/email_queue.json +++ b/frappe/email/doctype/email_queue/email_queue.json @@ -67,10 +67,9 @@ }, { "fieldname": "message_id", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Message ID", - "read_only": 1, - "search_index": 1 + "read_only": 1 }, { "fieldname": "reference_doctype", @@ -153,7 +152,7 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2022-07-12 15:17:37.934316", + "modified": "2023-03-16 12:15:17.850292", "modified_by": "Administrator", "module": "Email", "name": "Email Queue", diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 17fdddfaf3..b474f37e8e 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -407,6 +407,8 @@ def on_doctype_update(): "Email Queue", ("status", "send_after", "priority", "creation"), "index_bulk_flush" ) + frappe.db.add_index("Email Queue", ["message_id(140)"]) + def get_email_retry_limit(): return cint(frappe.db.get_system_setting("email_retry_limit")) or 3 diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 538ab7738d..59d41b543f 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -782,7 +782,7 @@ class InboundMail(Email): Here are the cases to handle: 1. If mail is a reply to already sent mail, then we can get parent communicaion from - Email Queue record. + Email Queue record or message_id on communication. 2. Sometimes we send communication name in message-ID directly, use that to get parent communication. 3. Sender sent a reply but reply is on top of what (s)he sent before, then parent record exists directly in communication. @@ -795,17 +795,15 @@ class InboundMail(Email): if not self.is_reply(): return "" - if not self.is_reply_to_system_sent_mail(): - communication = Communication.find_one_by_filters( - message_id=self.in_reply_to, creation=[">=", self.get_relative_dt(-30)] - ) - elif self.parent_email_queue() and self.parent_email_queue().communication: - communication = Communication.find(self.parent_email_queue().communication, ignore_error=True) - else: - reference = self.in_reply_to - if "@" in self.in_reply_to: - reference, _ = self.in_reply_to.split("@", 1) - communication = Communication.find(reference, ignore_error=True) + communication = Communication.find_one_by_filters(message_id=self.in_reply_to) + if not communication: + if self.parent_email_queue() and self.parent_email_queue().communication: + communication = Communication.find(self.parent_email_queue().communication, ignore_error=True) + else: + reference = self.in_reply_to + if "@" in self.in_reply_to: + reference, _ = self.in_reply_to.split("@", 1) + communication = Communication.find(reference, ignore_error=True) self._parent_communication = communication or "" return self._parent_communication From 035f7f93df2c083c152f0d77ffd205d11da8548e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 16 Mar 2023 15:56:19 +0530 Subject: [PATCH 033/120] fix: skip InReadOnlyMode in error snapshots (#20358) [skip ci] --- frappe/utils/error.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/utils/error.py b/frappe/utils/error.py index 235a9b3e67..2c450750e1 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -22,6 +22,7 @@ EXCLUDE_EXCEPTIONS = ( frappe.CSRFTokenError, # CSRF covers OAuth too frappe.SecurityException, LDAPException, + frappe.InReadOnlyMode, ) From ba99f746609d279fbb749343181e1688f2996ca9 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 16 Mar 2023 16:00:06 +0530 Subject: [PATCH 034/120] fix: handle image extraction while editing comment --- frappe/desk/form/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 9e10ced912..28377572c3 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -57,7 +57,14 @@ def update_comment(name, content): if frappe.session.user not in ["Administrator", doc.owner]: frappe.throw(_("Comment can only be edited by the owner"), frappe.PermissionError) - doc.content = content + if doc.reference_doctype and doc.reference_name: + reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name) + reference_doc.check_permission() + + doc.content = extract_images_from_html(reference_doc, content, is_private=True) + else: + doc.content = content + doc.save(ignore_permissions=True) From 0743225cd2817941b2e3defb43fd2559406db92a Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 16 Mar 2023 16:38:52 +0530 Subject: [PATCH 035/120] fix(patch): move desk prop patch to post model sync (#20361) * fix(patch): Reload user_email to avoid failure * fix: move patch to post sync --------- Co-authored-by: Ankush Menat [Skip ci] --- .../doctype/role/patches/v13_set_default_desk_properties.py | 2 -- frappe/patches.txt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py index 87de6ac79a..22854fa5f8 100644 --- a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py +++ b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py @@ -4,8 +4,6 @@ from ..role import desk_properties def execute(): - frappe.reload_doctype("user") - frappe.reload_doctype("role") for role in frappe.get_all("Role", ["name", "desk_access"]): role_doc = frappe.get_doc("Role", role.name) for key in desk_properties: diff --git a/frappe/patches.txt b/frappe/patches.txt index 9ebb32fea0..ab05ca673d 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -162,7 +162,6 @@ execute:frappe.delete_doc("DocType", "Footer Item") execute:frappe.reload_doctype('user') execute:frappe.reload_doctype('docperm') frappe.patches.v13_0.replace_field_target_with_open_in_new_tab -frappe.core.doctype.role.patches.v13_set_default_desk_properties frappe.patches.v13_0.add_switch_theme_to_navbar_settings frappe.patches.v13_0.update_icons_in_customized_desk_pages execute:frappe.db.set_default('desktop:home_page', 'space') @@ -199,6 +198,7 @@ frappe.patches.v15_0.remove_event_streaming frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report [post_model_sync] +frappe.core.doctype.role.patches.v13_set_default_desk_properties frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_github_endpoints #08-11-2021 From d7ab46f7f815ec70cb4359e487d4102bc3c41cec Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 16 Mar 2023 17:16:32 +0530 Subject: [PATCH 036/120] fix: allow 5 column layout in doctype form --- frappe/public/js/frappe/form/column.js | 7 ++++++- frappe/public/scss/desk/form.scss | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/column.js b/frappe/public/js/frappe/form/column.js index 3ea4296a94..317679f829 100644 --- a/frappe/public/js/frappe/form/column.js +++ b/frappe/public/js/frappe/form/column.js @@ -38,7 +38,12 @@ export default class Column { resize_all_columns() { // distribute all columns equally - let colspan = cint(12 / this.section.wrapper.find(".form-column").length); + let columns = this.section.wrapper.find(".form-column").length; + let colspan = cint(12 / columns); + + if (columns == 5) { + colspan = 20; + } this.section.wrapper .find(".form-column") diff --git a/frappe/public/scss/desk/form.scss b/frappe/public/scss/desk/form.scss index 516bc699c6..837f32f184 100644 --- a/frappe/public/scss/desk/form.scss +++ b/frappe/public/scss/desk/form.scss @@ -409,6 +409,21 @@ } } +// handle 5 columns in form +.form-column.col-sm-20 { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +@media (min-width: map-get($grid-breakpoints, "sm")) { + .form-column.col-sm-20 { + flex: 0 0 20%; + max-width: 20%; + } +} + // above mobile @media (min-width: map-get($grid-breakpoints, "md")) { .layout-main .form-column.col-sm-12 > form > .input-max-width { From f2f7d43811d3c90176594790b425cefdf8a429fd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 16 Mar 2023 20:07:43 +0530 Subject: [PATCH 037/120] fix: tolerant newsletter view logging (#20369) * fix: tolerant newsletter view logging * Revert "fix: tolerant newsletter view logging" This reverts commit 5a8567a9405f336c3dc8a481c27af70860334b2b. * fix: use raw query for updating count --- frappe/email/doctype/newsletter/newsletter.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index b5f07aa59c..43f767133c 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -377,14 +377,17 @@ def send_scheduled_email(): def newsletter_email_read(recipient_email, reference_doctype, reference_name): verify_request() try: - doc = frappe.get_doc(reference_doctype, reference_name) + doc = frappe.get_cached_doc("Newsletter", reference_name) if doc.add_viewed(recipient_email, force=True, unique_views=True): - doc.db_set("total_views", frappe.utils.cint(doc.total_views) + 1, update_modified=False) + newsletter = frappe.qb.DocType("Newsletter") + ( + frappe.qb.update(newsletter) + .set(newsletter.total_views, newsletter.total_views + 1) + .where(newsletter.name == doc.name) + ).run() except Exception: - frappe.log_error( - f"Unable to mark as viewed for {recipient_email}", None, reference_doctype, reference_name - ) + doc.log_error(f"Unable to mark as viewed for {recipient_email}") finally: frappe.response.update(frappe.utils.get_imaginary_pixel_response()) From cad9228b6b8c5fb6f447147237ad9200dcf9bed8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 17 Mar 2023 14:34:17 +0530 Subject: [PATCH 038/120] feat: clear integration log request logs (#20373) * fix: integration request should be cleared THese are basically logs of requests and shouldn't stay forever. * fix(UX): show retention policy message on sidebar These doctypes were added to log settings later. --- frappe/core/doctype/access_log/access_log_list.js | 7 +++++++ frappe/core/doctype/log_settings/log_settings.py | 1 + .../core/doctype/prepared_report/prepared_report_list.js | 7 +++++++ .../doctype/integration_request/integration_request.py | 7 +++++++ .../integration_request/integration_request_list.js | 7 +++++++ .../webhook_request_log/webhook_request_log_list.js | 7 +++++++ 6 files changed, 36 insertions(+) create mode 100644 frappe/core/doctype/access_log/access_log_list.js create mode 100644 frappe/core/doctype/prepared_report/prepared_report_list.js create mode 100644 frappe/integrations/doctype/integration_request/integration_request_list.js create mode 100644 frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js diff --git a/frappe/core/doctype/access_log/access_log_list.js b/frappe/core/doctype/access_log/access_log_list.js new file mode 100644 index 0000000000..dab5f083cb --- /dev/null +++ b/frappe/core/doctype/access_log/access_log_list.js @@ -0,0 +1,7 @@ +frappe.listview_settings["Access Log"] = { + onload: function (list_view) { + frappe.require("logtypes.bundle.js", () => { + frappe.utils.logtypes.show_log_retention_message(list_view.doctype); + }); + }, +}; diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index 4a3b457bcc..832be49f3c 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -20,6 +20,7 @@ DEFAULT_LOGTYPES_RETENTION = { "Submission Queue": 30, "Prepared Report": 30, "Webhook Request Log": 30, + "Integration Request": 90, "Reminder": 30, } diff --git a/frappe/core/doctype/prepared_report/prepared_report_list.js b/frappe/core/doctype/prepared_report/prepared_report_list.js new file mode 100644 index 0000000000..d0565fe826 --- /dev/null +++ b/frappe/core/doctype/prepared_report/prepared_report_list.js @@ -0,0 +1,7 @@ +frappe.listview_settings["Prepared Report"] = { + onload: function (list_view) { + frappe.require("logtypes.bundle.js", () => { + frappe.utils.logtypes.show_log_retention_message(list_view.doctype); + }); + }, +}; diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py index 334736bc9b..7ca185bd70 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.py +++ b/frappe/integrations/doctype/integration_request/integration_request.py @@ -13,6 +13,13 @@ class IntegrationRequest(Document): if self.flags._name: self.name = self.flags._name + def clear_old_logs(days=30): + from frappe.query_builder import Interval + from frappe.query_builder.functions import Now + + table = frappe.qb.DocType("Integration Request") + frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) + def update_status(self, params, status): data = json.loads(self.data) data.update(params) diff --git a/frappe/integrations/doctype/integration_request/integration_request_list.js b/frappe/integrations/doctype/integration_request/integration_request_list.js new file mode 100644 index 0000000000..9aede34f29 --- /dev/null +++ b/frappe/integrations/doctype/integration_request/integration_request_list.js @@ -0,0 +1,7 @@ +frappe.listview_settings["Integration Request"] = { + onload: function (list_view) { + frappe.require("logtypes.bundle.js", () => { + frappe.utils.logtypes.show_log_retention_message(list_view.doctype); + }); + }, +}; diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js b/frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js new file mode 100644 index 0000000000..dd4e215157 --- /dev/null +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js @@ -0,0 +1,7 @@ +frappe.listview_settings["Webhook Request Log"] = { + onload: function (list_view) { + frappe.require("logtypes.bundle.js", () => { + frappe.utils.logtypes.show_log_retention_message(list_view.doctype); + }); + }, +}; From 7ed43e2a68082ffa36933a0abf97fc7002696944 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 17 Mar 2023 15:57:51 +0530 Subject: [PATCH 039/120] fix: Drop message_id index before migrating email queue (#20376) [skip c] --- frappe/email/doctype/email_queue/patches/__init__.py | 0 .../patches/drop_search_index_on_message_id.py | 11 +++++++++++ frappe/patches.txt | 1 + 3 files changed, 12 insertions(+) create mode 100644 frappe/email/doctype/email_queue/patches/__init__.py create mode 100644 frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py diff --git a/frappe/email/doctype/email_queue/patches/__init__.py b/frappe/email/doctype/email_queue/patches/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py b/frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py new file mode 100644 index 0000000000..7c4baf5a2a --- /dev/null +++ b/frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + """Drop search index on message_id""" + + if frappe.db.get_column_type("Email Queue", "message_id") == "text": + return + + if index := frappe.db.get_column_index("tabEmail Queue", "message_id", unique=False): + frappe.db.sql(f"ALTER TABLE `tabEmail Queue` DROP INDEX `{index.Key_name}`") diff --git a/frappe/patches.txt b/frappe/patches.txt index ab05ca673d..da83094961 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -183,6 +183,7 @@ frappe.patches.v13_0.encrypt_2fa_secrets frappe.patches.v13_0.reset_corrupt_defaults frappe.patches.v13_0.remove_share_for_std_users execute:frappe.reload_doc('custom', 'doctype', 'custom_field') +frappe.email.doctype.email_queue.patches.drop_search_index_on_message_id frappe.patches.v14_0.update_workspace2 # 20.09.2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.transform_todo_schema From 98006eb5fb3c74b42e8252052807c06667556ab1 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 20 Mar 2023 06:27:40 +0000 Subject: [PATCH 040/120] chore: fix typo in error dump (#20393) [skip ci] --- frappe/public/js/frappe/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index dae436a84a..47ca1f7548 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -529,7 +529,7 @@ frappe.request.report_error = function (xhr, request_opts) { code_block(JSON.stringify(frappe.boot.versions, null, "\t")), "### Route", code_block(frappe.get_route_str()), - "### Trackeback", + "### Traceback", code_block(exc), "### Request Data", code_block(JSON.stringify(request_opts, null, "\t")), From 14105e08164aa097aaf1b49e11706a29dcaada40 Mon Sep 17 00:00:00 2001 From: phot0n Date: Wed, 25 Jan 2023 15:23:47 +0530 Subject: [PATCH 041/120] fix: refresh access token for dropbox - removed dropbox_access_token field - added dropbox_refresh_token - removed oauth2 access token generation from oauth1 logic - removed code for dropbox_erpnext_broker Auth logic has been changed a bit to generate access token(s) on the fly when taking backup from refresh token. This is due to the fact that backups are generally taken between long intervals which is generally greater than the access token expiry time. --- .../dropbox_settings/dropbox_settings.js | 55 ++++---- .../dropbox_settings/dropbox_settings.json | 14 +- .../dropbox_settings/dropbox_settings.py | 125 +++++------------- 3 files changed, 65 insertions(+), 129 deletions(-) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js index 9a5e9a4dc7..2b8b5c2db1 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -5,50 +5,43 @@ frappe.ui.form.on("Dropbox Settings", { refresh: function (frm) { frm.toggle_display( ["app_access_key", "app_secret_key"], - !(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) + !frm.doc.__onload?.dropbox_setup_via_site_config ); - frm.clear_custom_buttons(); frm.events.take_backup(frm); }, + are_keys_present: function (frm) { + return ( + (frm.doc.app_access_key && frm.doc.app_secret_key) || + frm.doc.__onload?.dropbox_setup_via_site_config + ); + }, + allow_dropbox_access: function (frm) { - if (frm.doc.app_access_key && frm.doc.app_secret_key) { - frappe.call({ - method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url", - freeze: true, - callback: function (r) { - if (!r.exc) { - window.open(r.message.auth_url); - } - }, - }); - } else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) { - frappe.call({ - method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_redirect_url", - freeze: true, - callback: function (r) { - if (!r.exc) { - window.open(r.message.auth_url); - } - }, - }); - } else { - frappe.msgprint(__("Please enter values for App Access Key and App Secret Key")); + if (!frm.events.are_keys_present(frm)) { + frappe.msgprint(__("No App Access Key and Secret Key are present.")); + return; } + + frappe.call({ + method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url", + freeze: true, + callback: function (r) { + if (!r.exc) { + window.open(r.message.auth_url); + } + }, + }); }, take_backup: function (frm) { - if ( - frm.doc.enabled && - ((frm.doc.app_access_key && frm.doc.app_secret_key) || - (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)) - ) { - frm.add_custom_button(__("Take Backup Now"), function (frm) { + if (frm.doc.enabled && frm.doc.dropbox_refresh_token) { + frm.add_custom_button(__("Take Backup Now"), function () { frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", freeze: true, }); - }).addClass("btn-primary"); + }); } }, }); diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json index 858469647a..664e51df90 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -1,8 +1,10 @@ { + "actions": [], "creation": "2016-09-21 10:12:57.399174", "doctype": "DocType", "document_type": "System", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "enabled", "send_notifications_to", @@ -16,7 +18,7 @@ "allow_dropbox_access", "dropbox_access_key", "dropbox_access_secret", - "dropbox_access_token" + "dropbox_refresh_token" ], "fields": [ { @@ -96,15 +98,18 @@ "read_only": 1 }, { - "fieldname": "dropbox_access_token", + "fieldname": "dropbox_refresh_token", "fieldtype": "Password", "hidden": 1, - "label": "Dropbox Access Token" + "label": "Dropbox Refresh Token", + "no_copy": 1, + "read_only": 1 } ], "in_create": 1, "issingle": 1, - "modified": "2019-08-22 16:26:44.468391", + "links": [], + "modified": "2023-01-26 20:43:14.458823", "modified_by": "Administrator", "module": "Integrations", "name": "Dropbox Settings", @@ -125,5 +130,6 @@ "read_only": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index e6998a9d6d..edf5f5e923 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE -import json import os from urllib.parse import parse_qs, urlparse @@ -16,16 +15,8 @@ from frappe.integrations.offsite_backup_utils import ( send_email, validate_file_size, ) -from frappe.integrations.utils import make_post_request from frappe.model.document import Document -from frappe.utils import ( - cint, - encode, - get_backups_path, - get_files_path, - get_request_site_address, - get_url, -) +from frappe.utils import cint, encode, get_backups_path, get_files_path, get_request_site_address from frappe.utils.background_jobs import enqueue from frappe.utils.backups import new_backup @@ -106,22 +97,13 @@ def backup_to_dropbox(upload_db_backup=True): # upload database dropbox_settings = get_dropbox_settings() - - if not dropbox_settings["access_token"]: - access_token = generate_oauth2_access_token_from_oauth1_token(dropbox_settings) - - if not access_token.get("oauth2_token"): - return ( - "Failed backup upload", - "No Access Token exists! Please generate the access token for Dropbox.", - ) - - dropbox_settings["access_token"] = access_token["oauth2_token"] - set_dropbox_access_token(access_token["oauth2_token"]) - dropbox_client = dropbox.Dropbox( - oauth2_access_token=dropbox_settings["access_token"], timeout=None + oauth2_refresh_token=dropbox_settings["refresh_token"], + app_key=dropbox_settings["app_key"], + app_secret=dropbox_settings["app_secret"], + timeout=None, ) + dropbox_client.refresh_access_token() if upload_db_backup: if frappe.flags.create_new_backup: @@ -267,24 +249,17 @@ def get_uploaded_files_meta(dropbox_folder, dropbox_client): # folder not found if isinstance(e.error, dropbox.files.ListFolderError): return frappe._dict({"entries": []}) - else: - raise + raise def get_dropbox_settings(redirect_uri=False): - if not frappe.conf.dropbox_broker_site: - frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com" settings = frappe.get_doc("Dropbox Settings") app_details = { "app_key": settings.app_access_key or frappe.conf.dropbox_access_key, "app_secret": settings.get_password(fieldname="app_secret_key", raise_exception=False) if settings.app_secret_key else frappe.conf.dropbox_secret_key, - "access_token": settings.get_password("dropbox_access_token", raise_exception=False) - if settings.dropbox_access_token - else "", - "access_key": settings.get_password("dropbox_access_key", raise_exception=False), - "access_secret": settings.get_password("dropbox_access_secret", raise_exception=False), + "refresh_token": settings.get_password("dropbox_refresh_token", raise_exception=False), "file_backup": settings.file_backup, "no_of_backups": settings.no_of_backups if settings.limit_no_of_backups else None, } @@ -294,14 +269,11 @@ def get_dropbox_settings(redirect_uri=False): { "redirect_uri": get_request_site_address(True) + "/api/method/frappe.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_auth_finish" - if settings.app_secret_key - else frappe.conf.dropbox_broker_site - + "/api/method/dropbox_erpnext_broker.www.setup_dropbox.generate_dropbox_access_token", } ) - if not app_details["app_key"] or not app_details["app_secret"]: - raise Exception(_("Please set Dropbox access keys in your site config")) + if not (app_details["app_key"] and app_details["app_secret"]): + raise Exception(_("Please set Dropbox access keys in site config or doctype")) return app_details @@ -321,28 +293,6 @@ def delete_older_backups(dropbox_client, folder_path, to_keep): dropbox_client.files_delete(os.path.join(folder_path, f.name)) -@frappe.whitelist() -def get_redirect_url(): - if not frappe.conf.dropbox_broker_site: - frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com" - url = "{}/api/method/dropbox_erpnext_broker.www.setup_dropbox.get_authotize_url".format( - frappe.conf.dropbox_broker_site - ) - - try: - response = make_post_request(url, data={"site": get_url()}) - if response.get("message"): - return response["message"] - - except Exception: - frappe.log_error() - frappe.throw( - _( - "Something went wrong while generating dropbox access token. Please check error log for more details." - ) - ) - - @frappe.whitelist() def get_dropbox_authorize_url(): app_details = get_dropbox_settings(redirect_uri=True) @@ -352,6 +302,7 @@ def get_dropbox_authorize_url(): session={}, csrf_token_session_key="dropbox-auth-csrf-token", consumer_secret=app_details["app_secret"], + token_access_type="offline", ) auth_url = dropbox_oauth_flow.start() @@ -360,11 +311,20 @@ def get_dropbox_authorize_url(): @frappe.whitelist() -def dropbox_auth_finish(return_access_token=False): +def dropbox_auth_finish(): app_details = get_dropbox_settings(redirect_uri=True) callback = frappe.form_dict close = '

' + _("Please close this window") + "

" + if not callback.state or not callback.code: + frappe.respond_as_web_page( + _("Dropbox Setup"), + _("Illegal Access Token. Please try again") + close, + indicator_color="red", + http_status_code=frappe.AuthenticationError.http_status_code, + ) + return + dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( consumer_key=app_details["app_key"], redirect_uri=app_details["redirect_uri"], @@ -373,40 +333,17 @@ def dropbox_auth_finish(return_access_token=False): consumer_secret=app_details["app_secret"], ) - if callback.state or callback.code: - token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code}) - if return_access_token and token.access_token: - return token.access_token, callback.state + token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code}) + set_dropbox_token(token.refresh_token) - set_dropbox_access_token(token.access_token) - else: - frappe.respond_as_web_page( - _("Dropbox Setup"), - _("Illegal Access Token. Please try again") + close, - indicator_color="red", - http_status_code=frappe.AuthenticationError.http_status_code, - ) - - frappe.respond_as_web_page( - _("Dropbox Setup"), _("Dropbox access is approved!") + close, indicator_color="green" - ) + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = "/app/dropbox-settings" -def set_dropbox_access_token(access_token): - frappe.db.set_single_value("Dropbox Settings", "dropbox_access_token", access_token) +def set_dropbox_token(refresh_token): + # NOTE: used doc object instead of db.set_value so that password field is set properly + dropbox_settings = frappe.get_single("Dropbox Settings") + dropbox_settings.dropbox_refresh_token = refresh_token + dropbox_settings.save() + frappe.db.commit() - - -def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None): - if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"): - return {} - - url = "https://api.dropboxapi.com/2/auth/token/from_oauth1" - headers = {"Content-Type": "application/json"} - auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"]) - data = { - "oauth1_token": dropbox_settings["access_key"], - "oauth1_token_secret": dropbox_settings["access_secret"], - } - - return make_post_request(url, auth=auth, headers=headers, data=json.dumps(data)) From 7445843bc2d4f4152d111ffd48098b0e5db6b67c Mon Sep 17 00:00:00 2001 From: phot0n Date: Thu, 2 Mar 2023 11:36:50 +0530 Subject: [PATCH 042/120] refactor: remove dropbox_access_key and dropbox_access_secret fields from dropbox settings --- .../dropbox_settings/dropbox_settings.json | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json index 664e51df90..426b1c5ab8 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -16,8 +16,6 @@ "app_access_key", "app_secret_key", "allow_dropbox_access", - "dropbox_access_key", - "dropbox_access_secret", "dropbox_refresh_token" ], "fields": [ @@ -83,20 +81,6 @@ "fieldtype": "Button", "label": "Allow Dropbox Access" }, - { - "fieldname": "dropbox_access_key", - "fieldtype": "Password", - "hidden": 1, - "label": "Dropbox Access Key", - "read_only": 1 - }, - { - "fieldname": "dropbox_access_secret", - "fieldtype": "Password", - "hidden": 1, - "label": "Dropbox Access Secret", - "read_only": 1 - }, { "fieldname": "dropbox_refresh_token", "fieldtype": "Password", @@ -109,7 +93,7 @@ "in_create": 1, "issingle": 1, "links": [], - "modified": "2023-01-26 20:43:14.458823", + "modified": "2023-03-02 11:36:35.459730", "modified_by": "Administrator", "module": "Integrations", "name": "Dropbox Settings", From e032a5b69bdd216bc1de45c89a0771afacc4974e Mon Sep 17 00:00:00 2001 From: phot0n Date: Sun, 12 Mar 2023 02:39:02 +0530 Subject: [PATCH 043/120] fix: add back dropbox access token reason: https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/Long-lived-access-token-deprecation/m-p/630471/highlight/true\#M29128 --- .../dropbox_settings/dropbox_settings.js | 4 +- .../dropbox_settings/dropbox_settings.json | 11 +++++- .../dropbox_settings/dropbox_settings.py | 39 ++++++++++++------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js index 2b8b5c2db1..6fee47f1bd 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -19,7 +19,7 @@ frappe.ui.form.on("Dropbox Settings", { allow_dropbox_access: function (frm) { if (!frm.events.are_keys_present(frm)) { - frappe.msgprint(__("No App Access Key and Secret Key are present.")); + frappe.msgprint(__("App Access Key and/or Secret Key are not present.")); return; } @@ -35,7 +35,7 @@ frappe.ui.form.on("Dropbox Settings", { }, take_backup: function (frm) { - if (frm.doc.enabled && frm.doc.dropbox_refresh_token) { + if (frm.doc.enabled && (frm.doc.dropbox_refresh_token || frm.doc.dropbox_access_token)) { frm.add_custom_button(__("Take Backup Now"), function () { frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json index 426b1c5ab8..15535f08c4 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -16,7 +16,8 @@ "app_access_key", "app_secret_key", "allow_dropbox_access", - "dropbox_refresh_token" + "dropbox_refresh_token", + "dropbox_access_token" ], "fields": [ { @@ -88,12 +89,18 @@ "label": "Dropbox Refresh Token", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "dropbox_access_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Token" } ], "in_create": 1, "issingle": 1, "links": [], - "modified": "2023-03-02 11:36:35.459730", + "modified": "2023-03-20 14:20:19.180611", "modified_by": "Administrator", "module": "Integrations", "name": "Dropbox Settings", diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index edf5f5e923..c213ba8340 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -92,18 +92,9 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): def backup_to_dropbox(upload_db_backup=True): - if not frappe.db: - frappe.connect() - # upload database dropbox_settings = get_dropbox_settings() - dropbox_client = dropbox.Dropbox( - oauth2_refresh_token=dropbox_settings["refresh_token"], - app_key=dropbox_settings["app_key"], - app_secret=dropbox_settings["app_secret"], - timeout=None, - ) - dropbox_client.refresh_access_token() + dropbox_client = get_dropbox_client(dropbox_settings) if upload_db_backup: if frappe.flags.create_new_backup: @@ -252,7 +243,25 @@ def get_uploaded_files_meta(dropbox_folder, dropbox_client): raise +def get_dropbox_client(dropbox_settings): + dropbox_client = dropbox.Dropbox( + oauth2_access_token=dropbox_settings["access_token"], + oauth2_refresh_token=dropbox_settings["refresh_token"], + app_key=dropbox_settings["app_key"], + app_secret=dropbox_settings["app_secret"], + timeout=None, + ) + + # checking if the access token has expired + dropbox_client.files_list_folder("") + if dropbox_settings["access_token"] != dropbox_client._oauth2_access_token: + set_dropbox_token(dropbox_client._oauth2_access_token) + + return dropbox_client + + def get_dropbox_settings(redirect_uri=False): + # NOTE: access token is kept for legacy dropbox apps settings = frappe.get_doc("Dropbox Settings") app_details = { "app_key": settings.app_access_key or frappe.conf.dropbox_access_key, @@ -260,6 +269,7 @@ def get_dropbox_settings(redirect_uri=False): if settings.app_secret_key else frappe.conf.dropbox_secret_key, "refresh_token": settings.get_password("dropbox_refresh_token", raise_exception=False), + "access_token": settings.get_password("dropbox_access_token", raise_exception=False), "file_backup": settings.file_backup, "no_of_backups": settings.no_of_backups if settings.limit_no_of_backups else None, } @@ -334,16 +344,19 @@ def dropbox_auth_finish(): ) token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code}) - set_dropbox_token(token.refresh_token) + set_dropbox_token(token.access_token, token.refresh_token) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = "/app/dropbox-settings" -def set_dropbox_token(refresh_token): +def set_dropbox_token(access_token, refresh_token=None): # NOTE: used doc object instead of db.set_value so that password field is set properly dropbox_settings = frappe.get_single("Dropbox Settings") - dropbox_settings.dropbox_refresh_token = refresh_token + dropbox_settings.dropbox_access_token = access_token + if refresh_token: + dropbox_settings.dropbox_refresh_token = refresh_token + dropbox_settings.save() frappe.db.commit() From eeeaedac65968473ecc3eeff79c76176836cc431 Mon Sep 17 00:00:00 2001 From: gavin Date: Mon, 20 Mar 2023 20:44:32 +0530 Subject: [PATCH 044/120] fix(workspace): Setup Dynamic Link if value exists (#20402) --- frappe/public/js/frappe/widgets/widget_dialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 0062e42ac5..45f7e5fc47 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -382,8 +382,8 @@ class ShortcutDialog extends WidgetDialog { reqd: 1, options: "type", onchange: () => { - if (this.dialog.get_value("type") == "DocType") { - let doctype = this.dialog.get_value("link_to"); + const doctype = this.dialog.get_value("link_to"); + if (doctype && this.dialog.get_value("type") == "DocType") { frappe.model.with_doctype(doctype, () => { let meta = frappe.get_meta(doctype); From 3da5a84d7c6382dd7f879ff93dd4b8e38fc9b29f Mon Sep 17 00:00:00 2001 From: gavin Date: Mon, 20 Mar 2023 21:00:03 +0530 Subject: [PATCH 045/120] fix(meta): get_permitted_fields with no field-columns (#20401) --- frappe/model/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index f6c6ee0a21..f171e4b1be 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -200,6 +200,10 @@ def get_permitted_fields( if doctype in core_doctypes_list: return valid_columns + # DocType has only fields of type Table (Table, Table MultiSelect) + if set(valid_columns).issubset(default_fields): + return valid_columns + if permitted_fields := meta.get_permitted_fieldnames(parenttype=parenttype, user=user): meta_fields = meta.default_fields.copy() optional_meta_fields = [x for x in optional_fields if x in valid_columns] From 12144505bc852b76a0b53370056a221457269d55 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Mar 2023 16:45:53 +0100 Subject: [PATCH 046/120] fix: clear contacts cache (#20397) --- frappe/cache_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 24a4c6a271..12e829ff09 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -63,6 +63,7 @@ user_cache_keys = ( "has_role:Page", "has_role:Report", "desk_sidebar_items", + "contacts", ) doctype_cache_keys = ( From d48a1c9e7c1ca42e185032be406c0aaccd9429d8 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Mar 2023 17:25:08 +0100 Subject: [PATCH 047/120] feat: make workflow state translatable (#20326) --- frappe/workflow/doctype/workflow_state/workflow_state.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/workflow/doctype/workflow_state/workflow_state.json b/frappe/workflow/doctype/workflow_state/workflow_state.json index e6b902938f..1b175058c3 100644 --- a/frappe/workflow/doctype/workflow_state/workflow_state.json +++ b/frappe/workflow/doctype/workflow_state/workflow_state.json @@ -40,10 +40,11 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2022-08-03 12:20:52.588427", + "modified": "2023-03-13 22:33:58.504532", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow State", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -66,5 +67,6 @@ "sort_field": "modified", "sort_order": "ASC", "states": [], - "track_changes": 1 + "track_changes": 1, + "translated_doctype": 1 } \ No newline at end of file From c9e12edb4259ad5d9c34d7579af0d2079bbf62ef Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 20 Mar 2023 23:41:18 +0530 Subject: [PATCH 048/120] feat: Add campaign and medium to web page view --- .../doctype/web_page_view/web_page_view.json | 20 ++++++++++++++++--- .../doctype/web_page_view/web_page_view.py | 4 ++++ .../website_analytics/website_analytics.js | 2 ++ frappe/www/website_script.js | 2 ++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/frappe/website/doctype/web_page_view/web_page_view.json b/frappe/website/doctype/web_page_view/web_page_view.json index 3eb0e7e722..2e514ffaec 100644 --- a/frappe/website/doctype/web_page_view/web_page_view.json +++ b/frappe/website/doctype/web_page_view/web_page_view.json @@ -13,8 +13,10 @@ "is_unique", "time_zone", "user_agent", - "visitor_id", - "source" + "source", + "campaign", + "medium", + "visitor_id" ], "fields": [ { @@ -68,11 +70,23 @@ "fieldtype": "Data", "label": "Source", "read_only": 1 + }, + { + "fieldname": "campaign", + "fieldtype": "Data", + "label": "Campaign", + "read_only": 1 + }, + { + "fieldname": "medium", + "fieldtype": "Data", + "label": "Medium", + "read_only": 1 } ], "in_create": 1, "links": [], - "modified": "2023-02-28 11:55:04.533663", + "modified": "2023-03-20 23:38:27.067285", "modified_by": "Administrator", "module": "Website", "name": "Web Page View", diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py index 5cd6b16bc8..bbf2a394a6 100644 --- a/frappe/website/doctype/web_page_view/web_page_view.py +++ b/frappe/website/doctype/web_page_view/web_page_view.py @@ -19,6 +19,8 @@ def make_view_log( version=None, user_tz=None, source=None, + campaign=None, + medium=None, visitor_id=None, ): if not is_tracking_enabled(): @@ -55,6 +57,8 @@ def make_view_log( view.user_agent = user_agent view.is_unique = is_unique view.source = source + view.campaign = campaign + view.medium = (medium or "").lower() view.visitor_id = visitor_id try: diff --git a/frappe/website/report/website_analytics/website_analytics.js b/frappe/website/report/website_analytics/website_analytics.js index d88a9de663..eabc4dee60 100644 --- a/frappe/website/report/website_analytics/website_analytics.js +++ b/frappe/website/report/website_analytics/website_analytics.js @@ -37,6 +37,8 @@ frappe.query_reports["Website Analytics"] = { { value: "browser", label: __("Browser") }, { value: "referrer", label: __("Referrer") }, { value: "source", label: __("Source") }, + { value: "campaign", label: __("Campaign") }, + { value: "medium", label: __("Medium") }, ], default: "path", }, diff --git a/frappe/www/website_script.js b/frappe/www/website_script.js index 17a8a3e8e1..a95b21313e 100644 --- a/frappe/www/website_script.js +++ b/frappe/www/website_script.js @@ -33,6 +33,8 @@ ga('send', 'pageview'); version: browser.version, user_tz: Intl.DateTimeFormat().resolvedOptions().timeZone, source: query_params.source, + medium: query_params.medium, + campaign: query_params.campaign, visitor_id: result.visitorId }) }) From ed7d6931ca208f89b8888e41d73fb57549ec845a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 20 Mar 2023 23:42:50 +0530 Subject: [PATCH 049/120] fix: Update code to update links with campaign and medium --- .../email/doctype/newsletter/newsletter.json | 12 +++- frappe/email/doctype/newsletter/newsletter.py | 12 ++-- .../newsletter/templates/newsletter.html | 2 +- frappe/utils/data.py | 15 ++++- .../doctype/marketing_campaign/__init__.py | 0 .../marketing_campaign.json | 64 +++++++++++++++++++ .../marketing_campaign/marketing_campaign.py | 9 +++ 7 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 frappe/website/doctype/marketing_campaign/__init__.py create mode 100644 frappe/website/doctype/marketing_campaign/marketing_campaign.json create mode 100644 frappe/website/doctype/marketing_campaign/marketing_campaign.py diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index 2b692ced04..cd292bebbd 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -29,6 +29,7 @@ "message", "message_md", "message_html", + "campaign", "attachments", "send_unsubscribe_link", "send_webview_link", @@ -237,6 +238,13 @@ "label": "Total Views", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "campaign", + "fieldtype": "Link", + "label": "Campaign", + "options": "Marketing Campaign", + "reqd": 1 } ], "has_web_view": 1, @@ -245,7 +253,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2023-02-23 12:53:18.478018", + "modified": "2023-03-20 22:45:59.129630", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", @@ -270,4 +278,4 @@ "states": [], "title_field": "subject", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 43f767133c..4a2f69a44c 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -167,7 +167,7 @@ class Newsletter(WebsiteGenerator): attachments = self.get_newsletter_attachments() sender = self.send_from or frappe.utils.get_formatted_email(self.owner) args = self.as_dict() - args["message"] = self.get_message() + args["message"] = self.get_message(medium="email") is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes) frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test @@ -193,7 +193,7 @@ class Newsletter(WebsiteGenerator): frappe.db.auto_commit_on_many_writes = is_auto_commit_set - def get_message(self) -> str: + def get_message(self, medium=None) -> str: message = self.message if self.content_type == "Markdown": message = frappe.utils.md_to_html(self.message_md) @@ -202,9 +202,9 @@ class Newsletter(WebsiteGenerator): html = frappe.render_template(message, {"doc": self.as_dict()}) - return self.add_source(html) + return self.add_source(html, medium=medium) - def add_source(self, html: str) -> str: + def add_source(self, html: str, medium="None") -> str: """Add source to the site links in the newsletter content.""" from bs4 import BeautifulSoup @@ -216,8 +216,8 @@ class Newsletter(WebsiteGenerator): if href and not href.startswith("#"): if not frappe.utils.is_site_link(href): continue - new_href = frappe.utils.add_source_to_url( - href, reference_doctype=self.doctype, reference_docname=self.name + new_href = frappe.utils.add_trackers_to_url( + href, source="Newsletter", campaign=self.campaign, medium=medium ) link["href"] = new_href diff --git a/frappe/email/doctype/newsletter/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html index 1244f4c49a..05f3560648 100644 --- a/frappe/email/doctype/newsletter/templates/newsletter.html +++ b/frappe/email/doctype/newsletter/templates/newsletter.html @@ -36,7 +36,7 @@

- {{ doc.get_message() }} + {{ doc.get_message(medium="web_page") }}
diff --git a/frappe/utils/data.py b/frappe/utils/data.py index d76d97f7e0..460ca26d85 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -2256,11 +2256,20 @@ def is_site_link(link: str) -> bool: return urlparse(link).netloc == urlparse(frappe.utils.get_url()).netloc -def add_source_to_url(url: str, reference_doctype: str, reference_docname: str) -> str: +def add_trackers_to_url(url: str, source: str, campaign: str, medium: str = "email") -> str: url_parts = list(urlparse(url)) - query = dict(parse_qsl(url_parts[4])) | { - "source": f"{reference_doctype} > {reference_docname}", + if url_parts[0] == "mailto": + return url + + trackers = { + "source": source, + "medium": medium, } + if campaign: + trackers["campaign"] = campaign + + query = dict(parse_qsl(url_parts[4])) | trackers + url_parts[4] = urlencode(query) return urlunparse(url_parts) diff --git a/frappe/website/doctype/marketing_campaign/__init__.py b/frappe/website/doctype/marketing_campaign/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/marketing_campaign/marketing_campaign.json b/frappe/website/doctype/marketing_campaign/marketing_campaign.json new file mode 100644 index 0000000000..0a5fc45b29 --- /dev/null +++ b/frappe/website/doctype/marketing_campaign/marketing_campaign.json @@ -0,0 +1,64 @@ +{ + "actions": [], + "autoname": "prompt", + "creation": "2023-03-20 22:36:45.058045", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_description" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "campaign_description", + "fieldtype": "Small Text", + "in_filter": 1, + "in_list_view": 1, + "label": "Campaign Description (Optional)" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-03-20 22:47:25.768582", + "modified_by": "Administrator", + "module": "Website", + "name": "Marketing Campaign", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Newsletter Manager", + "share": 1, + "write": 1 + }, + { + "role": "All", + "select": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/website/doctype/marketing_campaign/marketing_campaign.py b/frappe/website/doctype/marketing_campaign/marketing_campaign.py new file mode 100644 index 0000000000..ef23a182e7 --- /dev/null +++ b/frappe/website/doctype/marketing_campaign/marketing_campaign.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class MarketingCampaign(Document): + pass From 1abcd5a11a6336ae5408b61ea8d608e2f3ad6b82 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 20 Mar 2023 23:53:42 +0530 Subject: [PATCH 050/120] feat: Add a simple tool to generate tracking URL --- frappe/public/js/frappe/ui/toolbar/toolbar.js | 8 +-- frappe/public/js/frappe/utils/utils.js | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js index 8bf8f36f7a..419a22d764 100644 --- a/frappe/public/js/frappe/ui/toolbar/toolbar.js +++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js @@ -129,10 +129,10 @@ frappe.ui.toolbar.Toolbar = class { let awesome_bar = new frappe.search.AwesomeBar(); awesome_bar.setup("#navbar-search"); - // TODO: Remove this in v14 - frappe.search.utils.make_function_searchable(function () { - frappe.set_route("List", "Client Script"); - }, __("Custom Script List")); + frappe.search.utils.make_function_searchable( + frappe.utils.generate_tracking_url, + __("Generate Tracking URL") + ); } } diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 594da353e6..f849373229 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1610,4 +1610,56 @@ Object.assign(frappe.utils, { }); }, }, + generate_tracking_url() { + frappe.prompt( + [ + { + fieldname: "url", + label: __("Web Page URL"), + fieldtype: "Data", + options: "URL", + reqd: 1, + }, + { + fieldname: "source", + label: __("Source"), + fieldtype: "Data", + }, + { + fieldname: "campaign", + label: __("Campaign"), + fieldtype: "Link", + ignore_link_validation: 1, + options: "Marketing Campaign", + }, + { + fieldname: "medium", + label: __("Medium"), + fieldtype: "Data", + }, + ], + function (data) { + let url = data.url; + if (data.source) { + url += "?source=" + data.source; + } + if (data.campaign) { + url += "&campaign=" + data.campaign; + } + if (data.medium) { + url += "&medium=" + data.medium.toLowerCase(); + } + + frappe.utils.copy_to_clipboard(url); + + frappe.msgprint( + __("Tracking URL generated and copied to clipboard") + + ":
" + + `${url.bold()}`, + __("Here's your tracking URL") + ); + }, + __("Generate Tracking URL") + ); + }, }); From 02b661bbb9b85a7e73e93844d6b70a4ea6dd57fd Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 21 Mar 2023 11:31:05 +0530 Subject: [PATCH 051/120] fix: Remove mandatory and save URL params to localstorage - Saving URL params to localstorage to avoid re-entering the data. Usually only 1 or 2 param(s) change is required to generate new link --- frappe/email/doctype/newsletter/newsletter.json | 2 +- frappe/public/js/frappe/utils/utils.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index cd292bebbd..7ac6203ada 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -244,7 +244,7 @@ "fieldtype": "Link", "label": "Campaign", "options": "Marketing Campaign", - "reqd": 1 + "reqd": 0 } ], "has_web_view": 1, diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index f849373229..ac9a18785b 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1619,11 +1619,13 @@ Object.assign(frappe.utils, { fieldtype: "Data", options: "URL", reqd: 1, + default: localStorage.getItem("tracker_url:url"), }, { fieldname: "source", label: __("Source"), fieldtype: "Data", + default: localStorage.getItem("tracker_url:source"), }, { fieldname: "campaign", @@ -1631,23 +1633,30 @@ Object.assign(frappe.utils, { fieldtype: "Link", ignore_link_validation: 1, options: "Marketing Campaign", + default: localStorage.getItem("tracker_url:campaign"), }, { fieldname: "medium", label: __("Medium"), fieldtype: "Data", + default: localStorage.getItem("tracker_url:medium"), }, ], function (data) { let url = data.url; + localStorage.setItem("tracker_url:url", data.url); + if (data.source) { url += "?source=" + data.source; + localStorage.setItem("tracker_url:source", data.source); } if (data.campaign) { url += "&campaign=" + data.campaign; + localStorage.setItem("tracker_url:campaign", data.campaign); } if (data.medium) { url += "&medium=" + data.medium.toLowerCase(); + localStorage.setItem("tracker_url:medium", data.medium); } frappe.utils.copy_to_clipboard(url); From c5cfe8f5aad1df9f0fc34423ceba2d552ccf66cb Mon Sep 17 00:00:00 2001 From: Bernhard Sirlinger Date: Tue, 21 Mar 2023 08:25:03 +0100 Subject: [PATCH 052/120] feat(minor): log datetime in worker log (#20414) --- frappe/utils/background_jobs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 29098dce3b..47a821d8bb 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -243,7 +243,12 @@ def start_worker( if quiet: logging_level = "WARNING" worker = WorkerKlass(queues, name=get_worker_name(queue_name)) - worker.work(logging_level=logging_level, burst=burst) + worker.work( + logging_level=logging_level, + burst=burst, + date_format="%Y-%m-%d %H:%M:%S", + log_format="%(asctime)s,%(msecs)03d %(message)s", + ) def get_worker_name(queue): From 7fc6ae65aba13f277df98772f7ec65cea8c7e0cf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 21 Mar 2023 14:11:26 +0530 Subject: [PATCH 053/120] perf: Dont update list view data if list view not active (#20396) Steps to reproduce: 1. visit a list view that's quite active 2. move to some other page 3. list view data keeps getting refreshed in background Fix: Only refresh when user is back on list view --- frappe/public/js/frappe/list/list_view.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 4b6b1e5bc1..6a6e0ae0c8 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1354,6 +1354,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { process_document_refreshes() { if (!this.pending_document_refreshes.length) return; + const route = frappe.get_route() || []; + if (!cur_list || route[0] != "List" || cur_list.doctype != route[1]) { + // wait till user is back on list view before refreshing + this.pending_document_refreshes = []; + return; + } + const names = this.pending_document_refreshes.map((d) => d.name); this.pending_document_refreshes = this.pending_document_refreshes.filter( (d) => names.indexOf(d.name) === -1 From ef11d67bb3a1feaf5e0759f5de3adfc5917ec00d Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 21 Mar 2023 12:34:28 +0100 Subject: [PATCH 054/120] chore: translations of "Login" and "Signup" (german) (#20409) --- frappe/translations/de.csv | 49 ++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index a6aef52b1c..c60fcbb41e 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -38,8 +38,8 @@ Category Name,Kategoriename, City,Ort, City/Town,Ort/ Wohnort, Client,Kunde, -Client ID,Kunden-ID, -Client Secret,Kundengeheimnis, +Client ID,Client ID, +Client Secret,Client Secret, Closed,Geschlossen, Code,Code, Collapse All,Alles schließen, @@ -924,7 +924,6 @@ Domains HTML,Domänen HTML, "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field","Nicht HTML Encode HTML-Tags wie <script> oder einfach nur Zeichen wie <oder>, da sie absichtlich auf diesem Gebiet verwendet werden könnten,", Don't Override Status,Status nicht überschreiben, Don't create new records,Keine neuen Datensätze erstellen, -Don't have an account? Sign up,Sie haben noch kein Konto? Erstell' ein Konto, "Don't know, ask 'help'","Sie wissen nicht, fragen Sie "Hilfe"", Download Data,Daten herunterladen, Download Files Backup,Dateiensicherung herunterladen, @@ -1004,7 +1003,7 @@ Enable Print Server,Aktivieren Sie den Druckserver, Enable Raw Printing,Aktivieren Sie den RAW-Druck, Enable Report,Bericht aktivieren, Enable Scheduled Jobs,Geplante Arbeiten aktivieren, -Enable Social Login,Aktivieren Sie die soziale Anmeldung, +Enable Social Login,Aktivieren Sie Social Logins, Enable Two Factor Auth,Aktivieren Sie zwei Faktor Auth, Enabled email inbox for user {0},Aktivierter E-Mail-Posteingang für Benutzer {0}, "Encryption key is invalid, Please check site_config.json","Verschlüsselungsschlüssel ist ungültig, bitte check site_config.json", @@ -1217,7 +1216,7 @@ Has Attachments,Hat Anhänge, Has Domain,Hat Domain, Has Role,hat Rolle, Has Web View,Hat Webansicht, -Have an account? Login,Sie haben bereits ein Konto? Anmeldung, +Have an account? Login,Sie haben bereits ein Konto? Anmelden, Header,Kopfzeile, Header HTML,HTML-Header, Header HTML set from attachment {0},Header-HTML-Satz aus Anhang {0}, @@ -1321,7 +1320,7 @@ In seconds,In Sekunden, Include Search in Top Bar,Suchen In Top Bar, "Include symbols, numbers and capital letters in the password","Geben Sie Symbole, Zahlen und Großbuchstaben in das Passwort ein", Incoming email account not correct,Falsches Konto für eingehende E-Mails, -Incomplete login details,Unvollständige Login-Daten, +Incomplete login details,Unvollständige Anmeldedaten, Incorrect User or Password,Falscher Benutzer oder Passwort, Incorrect Verification code,Falscher Bestätigungscode, Incorrect value in row {0}: {1} must be {2} {3},Falscher Wert in Zeile {0}: {1} muss {2} {3} sein, @@ -1358,8 +1357,8 @@ Invalid Access Key ID or Secret Access Key.,Ungültige Zugriffsschlüssel-ID ode Invalid CSV Format,Ungültige CSV-Format, Invalid Home Page,Ungültige Startseite, Invalid Link,Ungültige Verknüpfung, -Invalid Login Token,Invalid Login Token, -Invalid Login. Try again.,Ungültiger Login. Versuch es noch einmal., +Invalid Login Token,Ungültiges Login-Token, +Invalid Login. Try again.,Ungültige Anmeldedaten. Bitte versuchen Sie es noch einmal., Invalid Mail Server. Please rectify and try again.,Ungültiger E-Mail-Server. Bitte Angaben korrigieren und erneut versuchen., Invalid Outgoing Mail Server or Port,Ungültiger Postausgangsserver oder Schnittstelle, Invalid Output Format,Ungültige Ausgabeformat, @@ -1374,7 +1373,7 @@ Invalid column,Ungültige Spalte, Invalid field name {0},Ungültiger Feldname {0}, Invalid fieldname '{0}' in autoname,Ungültige Feldname '{0}' in auton, Invalid file path: {0},Ungültiger Dateipfad: {0}, -Invalid login or password,Ungültige Benutzerkennung oder ungültiges Passwort, +Invalid login or password,Ungültiger Benutzer oder falsches Passwort, Invalid module path,Ungültiger Modulpfad, Invalid naming series (. missing),Ungültige Bezeichnungsserie (. fehlt), Invalid payment gateway credentials,Ungültige Payment-Gateway-Anmeldeinformationen, @@ -1514,11 +1513,11 @@ Login Id is required,Benutzer-ID wird benötigt, Login Required,Anmeldung erforderlich, Login Verification Code from {},Login-Bestätigungscode von {}, Login and view in Browser,Anmelden und im Browser anzeigen, -Login not allowed at this time,Anmelden zur Zeit nicht erlaubt, -"Login session expired, refresh page to retry","Anmeldesitzung abgelaufen, Seite aktualisieren, um es erneut zu versuchen", -Login to comment,Anmelden um Kommentieren zu können, +Login not allowed at this time,Anmelden zurzeit nicht erlaubt, +"Login session expired, refresh page to retry","Sitzung abgelaufen. Aktualisieren Sie die Seite, um es erneut zu versuchen", +Login to comment,"Anmelden, um Kommentieren zu können", Login token required,Login-Token erforderlich, -Login with LDAP,Einloggen mit LDAP, +Login with LDAP,Mit LDAP anmelden, Logout,Abmelden, Long Text,Langer Text, Looks like something is wrong with this site's Paypal configuration.,Sieht aus wie etwas ist falsch mit dieser Website Paypal-Konfiguration., @@ -1575,7 +1574,7 @@ Minimum Password Score,Mindest-Passwort-Score, Miss,Fräulein, Missing Fields,Nicht ausgefüllte Felder, Missing parameter Kanban Board Name,Fehlender Parameter Kanban Board Name, -Missing parameters for login,Fehlende Parameter für Login, +Missing parameters for login,Fehlende Parameter für Anmeldung, Models (building blocks) of the Application,Modelle (Bausteine) der Anwendung, Modified By,Geändert von, Module,Modul, @@ -1726,7 +1725,7 @@ Notification Recipient,Benachrichtigungsempfänger, Notification Tones,Benachrichtigungstöne, Notifications,Benachrichtigungen, Notifications and bulk mails will be sent from this outgoing server.,Hinweise und Massen-E-Mails werden von diesem Postausgangsserver versendet., -Notify Users On Every Login,Benachrichtige Benutzer bei jeder Anmeldung, +Notify Users On Every Login,Benutzer bei jeder Anmeldung benachrichtigen, Notify if unreplied,"Benachrichtigen, wenn unbeantwortet", Notify if unreplied for (in mins),"Benachrichtigen, wenn unbeantwortet für (in Minuten)", Notify users with a popup when they log in,"Benachrichtigen Sie die Benutzer mit einem Pop-up, wenn sie sich in", @@ -1785,7 +1784,6 @@ Options 'Dynamic Link' type of field must point to another Link Field with optio Options Help,Hilfe zu Optionen, Options for select. Each option on a new line.,Optionen zum Auswählen. Jede Option in einer neuen Zeile., Options not set for link field {0},Optionen nicht für das Verknüpfungs-Feld {0} gesetzt, -Or login with,Oder melden Sie sich an mit, Order,Auftrag, Org History,Unternehmensgeschichte, Org History Heading,Überschrift zur Unternehmensgeschichte, @@ -1880,8 +1878,8 @@ Please ensure that your profile has an email address,"Bitte stellen Sie sicher, Please enter Access Token URL,Bitte geben Sie die Access Token URL ein, Please enter Authorize URL,Bitte geben Sie die URL Autorisieren ein, Please enter Base URL,Bitte geben Sie die Basis-URL ein, -Please enter Client ID before social login is enabled,"Bitte geben Sie die Kunden-ID ein, bevor die Anmeldung mit sozialen Netzwerken aktiviert ist", -Please enter Client Secret before social login is enabled,"Bitte geben Sie das Kundengeheimnis ein, bevor die Anmeldung an soziale Netzwerke aktiviert ist", +Please enter Client ID before social login is enabled,"Bitte geben Sie die Client ID ein, bevor der Social Login aktiviert wird", +Please enter Client Secret before social login is enabled,"Bitte geben Sie das Client Secret ein, bevor Social Login aktiviert wird", Please enter Redirect URL,Bitte geben Sie die Weiterleitungs-URL ein, Please enter the password,Bitte Passwort eingeben, Please enter valid mobile nos,Bitte gültige Mobilnummern eingeben, @@ -1906,7 +1904,7 @@ Please select another payment method. Razorpay does not support transactions in Please select atleast 1 column from {0} to sort/group,Bitte wählen Sie atleast 1 Spalte von {0} sortieren / Gruppe, Please select document type first.,Bitte wählen Sie zuerst den Dokumententyp., Please select the Document Type.,Bitte wählen Sie den Dokumententyp., -Please set Base URL in Social Login Key for Frappe,Bitte legen Sie die Basis-URL unter Social Login-Schlüssel für Frappe fest, +Please set Base URL in Social Login Key for Frappe,Bitte legen Sie die Basis-URL im Social Login Key für Frappe fest, Please set Dropbox access keys in your site config,Bitte Dropbox-Zugriffsdaten in den Einstellungen der Seite setzen, Please set a printer mapping for this print format in the Printer Settings,Bitte legen Sie in den Druckereinstellungen eine Druckerzuordnung für dieses Druckformat fest, Please set filters,Bitte Filter einstellen, @@ -1992,7 +1990,7 @@ Push Update,Push-Aktualisierung, Python Module,Python-Modul, Pyver,Pyver, QR Code,QR-Code, -QR Code for Login Verification,QR Code für Login-Bestätigung, +QR Code for Login Verification,QR-Code für Zwei-Faktor-Authentifizierung, QZ Tray Connection Active!,QZ-Tray-Verbindung aktiv!, QZ Tray Failed: ,QZ-Fach fehlgeschlagen:, Quarter Day,Viertel-Tag, @@ -2328,8 +2326,7 @@ Showing only Numeric fields from Report,Nur numerische Felder aus Bericht anzeig Sidebar Items,Elemente der Seitenleiste, Sidebar Settings,Sidebar-Einstellungen, Sidebar and Comments,Sidebar und Kommentare, -Sign Up,Anmelden, -Sign Up is disabled,Registrieren ist deaktiviert, +Sign Up is disabled,Die Registrierung ist deaktiviert, Signature,Signatur, "Simple Python Expression, Example: Status in (""Closed"", ""Cancelled"")","Einfacher Python-Ausdruck, Beispiel: Status in ("Geschlossen", "Abgebrochen")", "Simple Python Expression, Example: status == 'Open' and type == 'Bug'","Einfacher Python-Ausdruck, Beispiel: status == 'Open' und type == 'Bug'", @@ -2353,7 +2350,7 @@ Smallest Currency Fraction Value,Kleinste Währungsanteilwert, Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01,"Kleinste zirkulierenden Brucheinheit (Münze). Für zB 1 Cent für USD und es sollte als 0,01 eingegeben werden", Snapshot View,Schnappschuss-Ansicht, Social,Sozial, -Social Login Key,Social Login-Schlüssel, +Social Login Key,Social Login Key, Social Login Provider,Social-Login-Anbieter, Social Logins,Soziale Logins, Socketio is not connected. Cannot upload,Socketio ist nicht verbunden. Kann nicht hochladen, @@ -3295,7 +3292,7 @@ Enable Email Notifications,E-Mail-Benachrichtigungen aktivieren, Enable Google API in Google Settings.,Aktivieren Sie die Google-API in den Google-Einstellungen., Enable Security,Sicherheit aktivieren, Energy Point,Energiepunkt, -Enter Client Id and Client Secret in Google Settings.,Geben Sie in den Google-Einstellungen die Kunden-ID und das Kundengeheimnis ein., +Enter Client Id and Client Secret in Google Settings.,Geben Sie in den Google-Einstellungen die Client ID und das Client Secret ein., Enter Code displayed in OTP App.,Geben Sie den in der OTP-App angezeigten Code ein., Event Configurations,Ereigniskonfigurationen, Event Consumer,Ereignis Verbraucher, @@ -3921,7 +3918,7 @@ Row #,Reihe #, Scheduled to send,Geplant zum Senden, Select Doctype,Wählen Sie Doctype, Send Email for Successful backup,Senden Sie eine E-Mail für eine erfolgreiche Sicherung, -Sign up,Anmeldung, +Sign up,Registrieren, Time format,Zeitformat, Upload failed,Upload fehlgeschlagen, User Id,Benutzeridentifikation, @@ -4502,7 +4499,7 @@ Footer Template Values,Werte für Fußzeilenvorlagen, Enable Tracking Page Views,Tracking-Seitenaufrufe aktivieren, "Checking this will enable tracking page views for blogs, web pages, etc.","Wenn Sie dies aktivieren, können Sie Seitenaufrufe für Blogs, Webseiten usw. verfolgen.", Disable Signup for your site,Deaktivieren Sie die Anmeldung für Ihre Site, -Check this if you don't want users to sign up for an account on your site. Users won't get desk access unless you explicitly provide it.,"Aktivieren Sie diese Option, wenn Benutzer sich nicht für ein Konto auf Ihrer Website anmelden sollen. Benutzer erhalten keinen Zugriff auf den Schreibtisch, es sei denn, Sie geben dies ausdrücklich an.", +Check this if you don't want users to sign up for an account on your site. Users won't get desk access unless you explicitly provide it.,"Aktivieren Sie diese Option, wenn Benutzer sich nicht für ein Konto auf Ihrer Website registrieren sollen. Benutzer erhalten keinen Zugriff auf die Anwendung, es sei denn, Sie geben dies ausdrücklich frei.", URL to go to on clicking the slideshow image,URL zum Aufrufen des Diashow-Bildes, Custom Overrides,Benutzerdefinierte Überschreibungen, Ignored Apps,Ignorierte Apps, From 516540ede9bee9b0c5e61c3984376e9daeb190ce Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 22 Mar 2023 10:02:37 +0530 Subject: [PATCH 055/120] perf: unsubscribe from list_update events (#20423) --- frappe/public/js/frappe/list/list_view.js | 10 +++++++++- frappe/public/js/frappe/socketio_client.js | 3 +++ socketio.js | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 6a6e0ae0c8..59784b853e 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -308,6 +308,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.render_header(refresh_header); this.update_checkbox(); this.update_url_with_filters(); + this.setup_realtime_updates(); }); } @@ -1329,7 +1330,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { setup_realtime_updates() { this.pending_document_refreshes = []; - if (this.list_view_settings && this.list_view_settings.disable_auto_refresh) { + if (this.list_view_settings?.disable_auto_refresh || this.realtime_events_setup) { return; } frappe.socketio.doctype_subscribe(this.doctype); @@ -1349,6 +1350,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.pending_document_refreshes.push(data); frappe.utils.debounce(this.process_document_refreshes.bind(this), 1000)(); }); + this.realtime_events_setup = true; + } + + disable_realtime_updates() { + frappe.socketio.doctype_unsubscribe(this.doctype); + this.realtime_events_setup = false; } process_document_refreshes() { @@ -1358,6 +1365,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (!cur_list || route[0] != "List" || cur_list.doctype != route[1]) { // wait till user is back on list view before refreshing this.pending_document_refreshes = []; + this.disable_realtime_updates(); return; } diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index d4a26f3188..f67da84ef4 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -132,6 +132,9 @@ frappe.socketio = { doctype_subscribe: function (doctype) { frappe.socketio.socket.emit("doctype_subscribe", doctype); }, + doctype_unsubscribe: function (doctype) { + frappe.socketio.socket.emit("doctype_unsubscribe", doctype); + }, doc_subscribe: function (doctype, docname) { if (frappe.flags.doc_subscribe) { console.log("throttled"); diff --git a/socketio.js b/socketio.js index 67746ee84e..8e87a0cce1 100644 --- a/socketio.js +++ b/socketio.js @@ -68,6 +68,10 @@ io.on("connection", function (socket) { }); }); + socket.on("doctype_unsubscribe", function (doctype) { + socket.leave(get_doctype_room(socket, doctype)); + }); + socket.on("task_subscribe", function (task_id) { var room = get_task_room(socket, task_id); socket.join(room); From 064ef5a15a5a7c3d85cfe7a794d38d5308474b7f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 22 Mar 2023 19:41:41 +0530 Subject: [PATCH 056/120] fix: Avoid list update if user is doing some bulk operation --- frappe/public/js/frappe/list/list_view.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 59784b853e..6b20282099 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1339,6 +1339,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { return; } + // if some bulk operation is happening by selecting list items, don't refresh + if (this.$checks && this.$checks.length) { + return; + } + if (!frappe.get_doc(data?.doctype, data?.name)?.__unsaved) { frappe.model.remove_from_locals(data.doctype, data.name); } From 7ac619921375f5962edab73eb7452c7edb3ff85f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Mar 2023 11:54:24 +0530 Subject: [PATCH 057/120] feat: re-enable scheduler from desk (#20434) [skip ci] --- frappe/core/doctype/rq_job/rq_job_list.js | 33 +++++++++++++++++++---- frappe/utils/scheduler.py | 5 ++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/rq_job/rq_job_list.js b/frappe/core/doctype/rq_job/rq_job_list.js index 5f6646cd65..fed56a16fe 100644 --- a/frappe/core/doctype/rq_job/rq_job_list.js +++ b/frappe/core/doctype/rq_job/rq_job_list.js @@ -4,11 +4,15 @@ frappe.listview_settings["RQ Job"] = { onload(listview) { if (!has_common(frappe.user_roles, ["Administrator", "System Manager"])) return; - listview.page.add_inner_button(__("Remove Failed Jobs"), () => { - frappe.confirm(__("Are you sure you want to remove all failed jobs?"), () => { - frappe.xcall("frappe.core.doctype.rq_job.rq_job.remove_failed_jobs"); - }); - }); + listview.page.add_inner_button( + __("Remove Failed Jobs"), + () => { + frappe.confirm(__("Are you sure you want to remove all failed jobs?"), () => { + frappe.xcall("frappe.core.doctype.rq_job.rq_job.remove_failed_jobs"); + }); + }, + __("Actions") + ); if (listview.list_view_settings) { listview.list_view_settings.disable_count = 1; @@ -20,6 +24,25 @@ frappe.listview_settings["RQ Job"] = { listview.page.set_indicator(__("Scheduler: Active"), "green"); } else { listview.page.set_indicator(__("Scheduler: Inactive"), "red"); + listview.page.add_inner_button( + __("Enable Scheduler"), + () => { + frappe.confirm(__("Are you sure you want to re-enable scheduler?"), () => { + frappe + .xcall("frappe.utils.scheduler.activate_scheduler") + .then(() => { + frappe.show_alert(__("Enabled Scheduler")); + }) + .catch((e) => { + frappe.show_alert({ + message: __("Failed to enable scheduler: {0}", e), + indicator: "error", + }); + }); + }); + }, + __("Actions") + ); } }); diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 8cda71ee9a..529a3c7bf7 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -176,6 +176,11 @@ def _get_last_modified_timestamp(doctype): @frappe.whitelist() def activate_scheduler(): + frappe.only_for("Administrator") + + if frappe.local.conf.maintenance_mode: + frappe.throw(frappe._("Scheduler can not be re-enabled when maintenance mode is active.")) + if is_scheduler_disabled(): enable_scheduler() if frappe.conf.pause_scheduler: From 2036dd19b23dad7859235877e5f0ebbf5b81e95f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 23 Mar 2023 12:30:50 +0530 Subject: [PATCH 058/120] fix: Get translated value for child tables in print format --- 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 5dcbaff7c5..2bfcf074ab 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -56,9 +56,9 @@ {% if doc.child_print_templates %} {%- set child_templates = doc.child_print_templates.get(df.fieldname) -%} -
{{ print_value(tdf, d, doc, visible_columns, child_templates) }}
+
{{ _(print_value(tdf, d, doc, visible_columns, child_templates)) }}
{% else %} -
{{ print_value(tdf, d, doc, visible_columns) }}
+
{{ _(print_value(tdf, d, doc, visible_columns)) }}
{% endif %} {% endfor %} From b39b2e7923c668105673e56467eb0087fe506b40 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Mar 2023 15:54:23 +0530 Subject: [PATCH 059/120] fix: consider JSON as Data for filters closes https://github.com/frappe/frappe/issues/20433 --- frappe/public/js/frappe/list/base_list.js | 1 + frappe/public/js/frappe/ui/filters/filter.js | 1 + 2 files changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 600db57dd1..8852a0df5d 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -774,6 +774,7 @@ class FilterArea { "Data", "Code", "Phone", + "JSON", "Read Only", ].includes(fieldtype) ) { diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index c2f795833c..5902c136bd 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -508,6 +508,7 @@ frappe.ui.filter_utils = { "HTML Editor", "Tag", "Phone", + "JSON", "Comments", "Barcode", "Dynamic Link", From 5a91ac945cb5afcccd005b706254105db6a4294d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Mar 2023 16:16:14 +0530 Subject: [PATCH 060/120] fix: doctype form - hide irrelevant fields --- frappe/core/doctype/doctype/doctype.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 671a6e86e6..b3196158f5 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -189,6 +189,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.istable", "fieldname": "beta", "fieldtype": "Check", "label": "Beta" @@ -463,6 +464,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.istable", "description": "Tree structures are implemented using Nested Set", "fieldname": "is_tree", "fieldtype": "Check", @@ -539,6 +541,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.istable", "fieldname": "is_virtual", "fieldtype": "Check", "label": "Is Virtual" @@ -622,6 +625,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.istable", "description": "Enables Calendar and Gantt views.", "fieldname": "is_calendar_and_gantt", "fieldtype": "Check", @@ -708,7 +712,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2023-01-04 17:23:09.206018", + "modified": "2023-03-23 16:15:51.067267", "modified_by": "Administrator", "module": "Core", "name": "DocType", From 3f717f2fbdc13571d409a99f94df25c78aa5886b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Mar 2023 16:55:09 +0530 Subject: [PATCH 061/120] perf: Don't re-initate sessions in realtime.py Session is already created for all requests in app.py -> init_request -> HTTPRequest() --- frappe/realtime.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/frappe/realtime.py b/frappe/realtime.py index 6283b8eb80..4a7cce0f45 100644 --- a/frappe/realtime.py +++ b/frappe/realtime.py @@ -105,10 +105,8 @@ def get_redis_server(): @frappe.whitelist(allow_guest=True) def can_subscribe_doc(doctype: str, docname: str) -> bool: from frappe.exceptions import PermissionError - from frappe.sessions import Session - session = Session(None, resume=True).get_session_data() - if not frappe.has_permission(user=session.user, doctype=doctype, doc=docname, ptype="read"): + if not frappe.has_permission(doctype=doctype, doc=docname, ptype="read"): raise PermissionError() return True @@ -118,7 +116,7 @@ def can_subscribe_doc(doctype: str, docname: str) -> bool: def can_subscribe_doctype(doctype: str) -> bool: from frappe.exceptions import PermissionError - if not frappe.has_permission(user=frappe.session.user, doctype=doctype, ptype="read"): + if not frappe.has_permission(doctype=doctype, ptype="read"): raise PermissionError() return True @@ -126,13 +124,9 @@ def can_subscribe_doctype(doctype: str) -> bool: @frappe.whitelist(allow_guest=True) def get_user_info(): - from frappe.sessions import Session - - session = Session(None, resume=True).get_session_data() - return { - "user": session.user, - "user_type": session.user_type, + "user": frappe.session.user, + "user_type": frappe.session.user_type, } From 6e3ef3dc3d0c166e1a02fcba6556e4aae1327b8c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 23 Mar 2023 17:53:18 +0530 Subject: [PATCH 062/120] test: fixed failing UI test --- cypress/integration/list_view.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 1fed62d678..3fa0758f0c 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -54,12 +54,8 @@ context("List View", () => { method: "POST", url: "api/method/frappe.model.workflow.bulk_workflow_approval", }).as("bulk-approval"); - cy.intercept({ - method: "POST", - url: "api/method/frappe.desk.reportview.get", - }).as("real-time-update"); cy.wrap(elements).contains("Approve").click(); - cy.wait(["@bulk-approval", "@real-time-update"]); + cy.wait("@bulk-approval"); cy.wait(300); cy.get_open_dialog().find(".btn-modal-close").click(); cy.reload(); From bd049a42a317b22d0c961ff0a12d26e123e41148 Mon Sep 17 00:00:00 2001 From: Leonard Goertz <49870752+uepselon@users.noreply.github.com> Date: Fri, 24 Mar 2023 04:14:34 +0100 Subject: [PATCH 063/120] fix: fields are rendered empty after save (#20270) Co-authored-by: Ankush Menat --- frappe/public/js/frappe/list/list_view.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 6b20282099..38dc76ec80 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1344,10 +1344,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { return; } - if (!frappe.get_doc(data?.doctype, data?.name)?.__unsaved) { - frappe.model.remove_from_locals(data.doctype, data.name); - } - if (this.avoid_realtime_update()) { return; } From f7f575acbdfb48e086f1fcd39118ddd58c751fd3 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 24 Mar 2023 04:23:34 +0100 Subject: [PATCH 064/120] ci: copy docs checker from erpnext (#20441) [skip ci] --- .github/helper/documentation.py | 97 ++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index 8156137e3f..b541583fd6 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -1,50 +1,73 @@ import sys +import requests from urllib.parse import urlparse -import requests -docs_repos = [ - "frappe_docs", - "erpnext_documentation", +WEBSITE_REPOS = [ "erpnext_com", "frappe_io", ] +DOCUMENTATION_DOMAINS = [ + "docs.erpnext.com", + "frappeframework.com", +] -def uri_validator(x): - result = urlparse(x) - return all([result.scheme, result.netloc, result.path]) -def docs_link_exists(body): - for line in body.splitlines(): - for word in line.split(): - if word.startswith('http') and uri_validator(word): - parsed_url = urlparse(word) - if parsed_url.netloc == "github.com": - parts = parsed_url.path.split('/') - if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: - return True - if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]: - return True +def is_valid_url(url: str) -> bool: + parts = urlparse(url) + return all((parts.scheme, parts.netloc, parts.path)) + + +def is_documentation_link(word: str) -> bool: + if not word.startswith("http") or not is_valid_url(word): + return False + + parsed_url = urlparse(word) + if parsed_url.netloc in DOCUMENTATION_DOMAINS: + return True + + if parsed_url.netloc == "github.com": + parts = parsed_url.path.split("/") + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS: + return True + + return False + + +def contains_documentation_link(body: str) -> bool: + return any( + is_documentation_link(word) + for line in body.splitlines() + for word in line.split() + ) + + +def check_pull_request(number: str) -> "tuple[int, str]": + response = requests.get(f"https://api.github.com/repos/frappe/frappe/pulls/{number}") + if not response.ok: + return 1, "Pull Request Not Found! ⚠️" + + payload = response.json() + title = (payload.get("title") or "").lower().strip() + head_sha = (payload.get("head") or {}).get("sha") + body = (payload.get("body") or "").lower() + + if ( + not title.startswith("feat") + or not head_sha + or "no-docs" in body + or "backport" in body + ): + return 0, "Skipping documentation checks... 🏃" + + if contains_documentation_link(body): + return 0, "Documentation Link Found. You're Awesome! 🎉" + + return 1, "Documentation Link Not Found! ⚠️" if __name__ == "__main__": - pr = sys.argv[1] - response = requests.get(f"https://api.github.com/repos/frappe/frappe/pulls/{pr}") - - if response.ok: - payload = response.json() - title = (payload.get("title") or "").lower() - head_sha = (payload.get("head") or {}).get("sha") - body = (payload.get("body") or "").lower() - - if title.startswith("feat") and head_sha and "no-docs" not in body: - if docs_link_exists(body): - print("Documentation Link Found. You're Awesome! 🎉") - - else: - print("Documentation Link Not Found! ⚠️") - sys.exit(1) - - else: - print("Skipping documentation checks... 🏃") + exit_code, message = check_pull_request(sys.argv[1]) + print(message) + sys.exit(exit_code) From 6b3d283cf7ac7ce26907c67fe229fecf356388ef Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Mar 2023 09:58:19 +0530 Subject: [PATCH 065/120] fix: unsubscribe from list_update before resubbing (#20450) resubbing can result in multiple events being fired, so unsubscribe all of them before re-subscribing. missed in https://github.com/frappe/frappe/pull/20423 --- frappe/public/js/frappe/list/list_view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 38dc76ec80..a7cd09d76d 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1334,6 +1334,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { return; } frappe.socketio.doctype_subscribe(this.doctype); + frappe.realtime.off("list_update"); frappe.realtime.on("list_update", (data) => { if (data?.doctype !== this.doctype) { return; From 0374d37c624c0d4d406092f361641c9316d850f0 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Fri, 24 Mar 2023 04:13:09 -0300 Subject: [PATCH 066/120] feat: show if address is "Disabled" (#20446) --- frappe/public/js/frappe/form/templates/address_list.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/form/templates/address_list.html b/frappe/public/js/frappe/form/templates/address_list.html index a6b8bf1377..0e533ed2ea 100644 --- a/frappe/public/js/frappe/form/templates/address_list.html +++ b/frappe/public/js/frappe/form/templates/address_list.html @@ -8,6 +8,8 @@ ({%= __("Primary") %}){% } %} {% if(addr_list[i].is_shipping_address) { %} ({%= __("Shipping") %}){% } %} + {% if(addr_list[i].disabled) { %} + ({%= __("Disabled") %}){% } %} From 0ec7ea45e9b3b291c84fb86026f31105b7df68b5 Mon Sep 17 00:00:00 2001 From: Himanshu Shivhare Date: Fri, 24 Mar 2023 12:49:42 +0530 Subject: [PATCH 067/120] fix: Update social media links (#20400) * YouTube channel addded in about section. I have added ERPNext YouTube channel reference in the about section. #Create an official Handle for YouTube Channel (Like @erpnext or erpnextofficial) * chore: handle [skip ci] * Update about.js YouTube channel name changed. --------- [skip ci] --- frappe/public/js/frappe/ui/toolbar/about.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/toolbar/about.js b/frappe/public/js/frappe/ui/toolbar/about.js index 69cbbfaba0..699db266da 100644 --- a/frappe/public/js/frappe/ui/toolbar/about.js +++ b/frappe/public/js/frappe/ui/toolbar/about.js @@ -20,7 +20,7 @@ frappe.ui.misc.about = function () {

Twitter: https://twitter.com/erpnext

- YouTube: https://www.youtube.com/@erpnextofficial

+ YouTube: https://www.youtube.com/@frappetech


${__("Installed Apps")}

${__("Loading versions...")}
From 543a4c467bdcb91cd5d2e236208c673e7908d5fd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Mar 2023 12:50:22 +0530 Subject: [PATCH 068/120] fix: social media links [skip ci] --- frappe/public/js/frappe/ui/toolbar/about.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/toolbar/about.js b/frappe/public/js/frappe/ui/toolbar/about.js index 699db266da..b75f950d72 100644 --- a/frappe/public/js/frappe/ui/toolbar/about.js +++ b/frappe/public/js/frappe/ui/toolbar/about.js @@ -18,7 +18,7 @@ frappe.ui.misc.about = function () {

Facebook: https://facebook.com/erpnext

- Twitter: https://twitter.com/erpnext

+ Twitter: https://twitter.com/frappetech

YouTube: https://www.youtube.com/@frappetech


From d9383afae692bfaa83c333b12eef17868888e7e9 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Fri, 24 Mar 2023 13:45:46 +0530 Subject: [PATCH 069/120] fix: exception handling for bulk email sending (#20451) --- frappe/email/doctype/email_queue/email_queue.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index b474f37e8e..c10494b0d9 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -5,6 +5,7 @@ import json import quopri import smtplib import traceback +from contextlib import suppress from email.parser import Parser from email.policy import SMTPUTF8 @@ -706,7 +707,10 @@ class QueueBuilder: if not smtp_server_instance: email_account = q.get_email_account() smtp_server_instance = email_account.get_smtp_server() - q.send(smtp_server_instance=smtp_server_instance) + + with suppress(Exception): + q.send(smtp_server_instance=smtp_server_instance) + smtp_server_instance.quit() def as_dict(self, include_recipients=True): From 86f72195251c5abfcacf5f9db0351fb0b54f8e1c Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 24 Mar 2023 21:02:30 +0530 Subject: [PATCH 070/120] fix: Auto-Reload after changing time zone (#20456) --- frappe/core/doctype/user/user.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 18e8651819..413dd07dc4 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -114,12 +114,16 @@ frappe.ui.form.on("User", { return; } + function hasChanged(doc_attr, boot_attr) { + return (doc_attr || boot_attr) && doc_attr !== boot_attr; + } + if ( doc.name === frappe.session.user && !doc.__unsaved && frappe.all_timezones && - (doc.language || frappe.boot.user.language) && - doc.language !== frappe.boot.user.language + (hasChanged(doc.language, frappe.boot.user.language) || + hasChanged(doc.time_zone, frappe.boot.time_zone.user)) ) { frappe.msgprint(__("Refreshing...")); window.location.reload(); From d2f529462bb2425a51205b2e16c521d8c44734c6 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Sat, 25 Mar 2023 13:07:05 +0530 Subject: [PATCH 071/120] fix: cannot restore cancelled document if workflow is active --- frappe/core/doctype/deleted_document/deleted_document.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py index 14b9bb5c11..3d4afa40ab 100644 --- a/frappe/core/doctype/deleted_document/deleted_document.py +++ b/frappe/core/doctype/deleted_document/deleted_document.py @@ -27,6 +27,8 @@ def restore(name, alert=True): except frappe.DocstatusTransitionError: frappe.msgprint(_("Cancelled Document restored as Draft")) doc.docstatus = 0 + if doc.workflow_state: + doc.workflow_state = None doc.insert() doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name)) From 08c02907771f06258ae25e59897bd7de59a1e922 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 26 Mar 2023 00:17:22 +0530 Subject: [PATCH 072/120] fix: don't hide tab with dashboard if there is a visible section/control --- frappe/public/js/frappe/form/tab.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/tab.js b/frappe/public/js/frappe/form/tab.js index ac5ee0f7c7..7ededcdd28 100644 --- a/frappe/public/js/frappe/form/tab.js +++ b/frappe/public/js/frappe/form/tab.js @@ -42,8 +42,8 @@ export default class Tab { hide = true; } - if (!hide && !this.df.show_dashboard) { - // show only if there is at least one visibe section or control + if (!hide) { + // show only if there is at least one visible section or control hide = true; if ( this.wrapper.find( @@ -54,11 +54,6 @@ export default class Tab { } } - // hide if dashboard and not saved - if (!hide && this.df.show_dashboard && this.frm.is_new()) { - hide = true; - } - this.toggle(!hide); } From f089e9108e5d71ab1fc2061ebe77e06f2b743135 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 27 Mar 2023 15:58:43 +0530 Subject: [PATCH 073/120] fix: hard link environment variable (#20467) On docker based deploys symlinking inside volume doesn't work. [skip ci] --- frappe/commands/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index f41cca3c57..01de67eb05 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -24,7 +24,11 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True} @click.option("--app", help="Build assets for app") @click.option("--apps", help="Build assets for specific apps") @click.option( - "--hard-link", is_flag=True, default=False, help="Copy the files instead of symlinking" + "--hard-link", + is_flag=True, + default=False, + help="Copy the files instead of symlinking", + envvar="FRAPPE_HARD_LINK_ASSETS", ) @click.option( "--make-copy", From 32dbbb47bfc5fa89647befd1b3cdf21f5c27b9f9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 27 Mar 2023 17:03:20 +0530 Subject: [PATCH 074/120] feat: redis cache decorator (#20452) * feat: redis cache decorator * fix: review changes * fix: remove unintentional changes * fix: remove unintentional changes * refactor: cleanup and simplify code for redis AIs suck * fix: bug * test: redis cache * fix: remove unused import * feat: make redis cache user specific redis cache utils already support this, extending so everyone can use it * feat: support @redis_cache without params * test: flake in request site cache test --------- Co-authored-by: Ankush Menat --- frappe/tests/test_caching.py | 79 +++++++++++++++++++++++++++++++++-- frappe/utils/caching.py | 36 ++++++++++++++++ frappe/utils/redis_wrapper.py | 4 ++ 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py index d1de587d0d..4faade331c 100644 --- a/frappe/tests/test_caching.py +++ b/frappe/tests/test_caching.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import frappe from frappe.tests.test_api import FrappeAPITestCase from frappe.tests.utils import FrappeTestCase -from frappe.utils.caching import request_cache, site_cache +from frappe.utils.caching import redis_cache, request_cache, site_cache CACHE_TTL = 4 external_service = MagicMock(return_value=30) @@ -82,13 +82,84 @@ class TestSiteCache(FrappeAPITestCase): api_with_ttl = f"{module}.ping_with_ttl" api_without_ttl = f"{module}.ping" - start = time.monotonic() for _ in range(5): self.get(f"/api/method/{api_with_ttl}") self.get(f"/api/method/{api_without_ttl}") - end = time.monotonic() self.assertEqual(register_with_external_service.call_count, 2) - time.sleep(CACHE_TTL - (end - start)) + time.sleep(CACHE_TTL) self.get(f"/api/method/{api_with_ttl}") self.assertEqual(register_with_external_service.call_count, 3) + + +class TestRedisCache(FrappeAPITestCase): + def test_redis_cache(self): + function_call_count = 0 + + @redis_cache(ttl=CACHE_TTL) + def calculate_area(radius: float) -> float: + nonlocal function_call_count + function_call_count += 1 + return 3.14 * radius**2 + + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 1) + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 1) + + time.sleep(CACHE_TTL) + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 2) + + calculate_area.clear_cache() + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 3) + calculate_area.clear_cache() + + def test_redis_cache_without_params(self): + function_call_count = 0 + + @redis_cache + def calculate_area(radius: float) -> float: + nonlocal function_call_count + function_call_count += 1 + return 3.14 * radius**2 + + calculate_area.clear_cache() + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 1) + + calculate_area.clear_cache() + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 2) + + calculate_area.clear_cache() + + def test_redis_cache_diff_args(self): + function_call_count = 0 + + @redis_cache(ttl=CACHE_TTL) + def calculate_area(radius: float) -> float: + nonlocal function_call_count + function_call_count += 1 + return 3.14 * radius**2 + + self.assertEqual(calculate_area(10), 314) + self.assertEqual(function_call_count, 1) + self.assertEqual(calculate_area(100), 31400) + self.assertEqual(function_call_count, 2) + + self.assertEqual(calculate_area(5), 25 * 3.14) + self.assertEqual(function_call_count, 3) + + calculate_area(10) + # from cache now + self.assertEqual(function_call_count, 3) + + calculate_area(radius=10) + # args, kwargs are treated differently + self.assertEqual(function_call_count, 4) + + calculate_area(radius=10) + # kwargs should hit cache too + self.assertEqual(function_call_count, 4) diff --git a/frappe/utils/caching.py b/frappe/utils/caching.py index a2c9496098..007582f25f 100644 --- a/frappe/utils/caching.py +++ b/frappe/utils/caching.py @@ -128,3 +128,39 @@ def site_cache(ttl: int | None = None, maxsize: int | None = None) -> Callable: return time_cache_wrapper(ttl) return time_cache_wrapper + + +def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Callable: + """Decorator to cache method calls and its return values in Redis + + args: + ttl: time to expiry in seconds, defaults to 1 hour + user: `true` should cache be specific to session user. + """ + + def wrapper(func: Callable = None) -> Callable: + + func_key = f"{func.__module__}.{func.__qualname__}" + + def clear_cache(): + frappe.cache().delete_keys(func_key) + + func.clear_cache = clear_cache + func.ttl = ttl if not callable(ttl) else 3600 + + @wraps(func) + def redis_cache_wrapper(*args, **kwargs): + func_call_key = func_key + str(__generate_request_cache_key(args, kwargs)) + if frappe.cache().exists(func_call_key): + return frappe.cache().get_value(func_call_key, user=user) + else: + val = func(*args, **kwargs) + ttl = getattr(func, "ttl", 3600) + frappe.cache().set_value(func_call_key, val, expires_in_sec=ttl, user=user) + return val + + return redis_cache_wrapper + + if callable(ttl): + return wrapper(ttl) + return wrapper diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index ea91299cfc..3b335b2c1d 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -195,6 +195,10 @@ class RedisWrapper(redis.Redis): except redis.exceptions.ConnectionError: return False + def exists(self, *names: str, user=None, shared=None) -> int: + names = [self.make_key(n, user=user, shared=shared) for n in names] + return super().exists(*names) + def hgetall(self, name): value = super().hgetall(self.make_key(name)) return {key: pickle.loads(value) for key, value in value.items()} From 69b22f3af244d52a3f50fb1839ae257b4420e81e Mon Sep 17 00:00:00 2001 From: Rutwik Hiwalkar <50401596+rutwikhdev@users.noreply.github.com> Date: Tue, 28 Mar 2023 00:01:42 +0530 Subject: [PATCH 075/120] chore: add ascending order to querybuilder (#20471) --- frappe/query_builder/builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/query_builder/builder.py b/frappe/query_builder/builder.py index 5a7b06221f..535284f34d 100644 --- a/frappe/query_builder/builder.py +++ b/frappe/query_builder/builder.py @@ -12,6 +12,7 @@ from frappe.utils import get_table_name class Base: terms = terms desc = Order.desc + asc = Order.asc Schema = Schema Table = Table From dc4509796d8284803d661125cc1a96bb10300a0b Mon Sep 17 00:00:00 2001 From: Samuel Danieli <23150094+scdanieli@users.noreply.github.com> Date: Tue, 28 Mar 2023 08:17:56 +0200 Subject: [PATCH 076/120] fix: suggested email ids in New Email dialog (#20319) * chore: enhance UX of New Email dialog * do not show contacts without an email * use name as value, y? if several contacts use the same email address, the entry will be displayed several times, but always with the same description, which leads to confusion - using name as value makes the entries distinguishable * chore: ignore semgrep Rewriting the query is not in the scope of this PR. * chore: keep semgrep failing on raw query [skip ci] * fix: use email_id as value * Revert "fix: use email_id as value" This reverts commit e4c44e2094ddb9b525bc34d400642dcee5656096. * chore: comment confusing code --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Co-authored-by: Ankush Menat --- frappe/email/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index 1f6af4a3e7..486db2a784 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -11,7 +11,7 @@ def sendmail_to_system_managers(subject, content): @frappe.whitelist() def get_contact_list(txt, page_length=20) -> list[dict]: - """Returns contacts (from autosuggest)""" + """Return email ids for a multiselect field.""" if cached_contacts := get_cached_contacts(txt): return cached_contacts[:page_length] @@ -19,11 +19,14 @@ def get_contact_list(txt, page_length=20) -> list[dict]: reportview_conditions = build_match_conditions("Contact") match_conditions = f"and {reportview_conditions}" if reportview_conditions else "" + # The multiselect field will store the `label` as the selected value. + # The `value` is just used as a unique key to distinguish between the options. + # https://github.com/frappe/frappe/blob/6c6a89bcdd9454060a1333e23b855d0505c9ebc2/frappe/public/js/frappe/form/controls/autocomplete.js#L29-L35 out = frappe.db.sql( - f"""select email_id as value, + f"""select name as value, email_id as label, concat(first_name, ifnull(concat(' ',last_name), '' )) as description from tabContact - where name like %(txt)s or email_id like %(txt)s + where (name like %(txt)s or email_id like %(txt)s) and email_id != '' {match_conditions} limit %(page_length)s""", {"txt": f"%{txt}%", "page_length": page_length}, From 229dcb3c9123ebaf5417e5959f25f7a21e50231e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 28 Mar 2023 12:07:13 +0530 Subject: [PATCH 077/120] fix: pin pymysql to avoid breaking behaviour (#20475) ``` File "/home/ankush/benches/develop/apps/frappe/frappe/database/database.py", line 920, in get_default d = self.get_defaults(key, parent) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ankush/benches/develop/apps/frappe/frappe/database/database.py", line 936, in get_defaults defaults = frappe.defaults.get_defaults_for(parent) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ankush/benches/develop/apps/frappe/frappe/defaults.py", line 222, in get_defaults_for .run(as_dict=True) ^^^^^^^^^^^^^^^^^ File "/home/ankush/benches/develop/apps/frappe/frappe/query_builder/utils.py", line 85, in execute_query return frappe.db.sql(query, params, *args, **kwargs) # nosemgrep ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ankush/benches/develop/apps/frappe/frappe/database/database.py", line 264, in sql self.log_query(query, values, debug, explain) File "/home/ankush/benches/develop/apps/frappe/frappe/database/mariadb/database.py", line 203, in log_query self.last_query = query = self._cursor._last_executed ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'Cursor' object has no attribute '_last_executed'. Did you mean: '_check_executed'? ``` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 837ea4624a..c984ca7e91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "Jinja2~=3.1.2", "Pillow~=9.3.0", "PyJWT~=2.4.0", - "PyMySQL~=1.0.2", + "PyMySQL==1.0.2", "PyPDF2~=2.1.0", "PyPika~=0.48.9", "PyQRCode~=1.2.1", From 024faff02505cc5764d822ecab718814ea75f242 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 28 Mar 2023 13:04:27 +0530 Subject: [PATCH 078/120] build: bump pymysql (#20478) Actual fix for this bandaid fix: https://github.com/frappe/frappe/pull/20475 Keeping pymysql hard pinned until we have better way to get last full query. --- frappe/database/mariadb/database.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 7f9846d6c4..43540956e0 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -200,7 +200,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): return db_size[0].get("database_size") def log_query(self, query, values, debug, explain): - self.last_query = query = self._cursor._last_executed + self.last_query = query = self._cursor._executed self._log_query(query, debug, explain) return self.last_query diff --git a/pyproject.toml b/pyproject.toml index c984ca7e91..48903f3163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "Jinja2~=3.1.2", "Pillow~=9.3.0", "PyJWT~=2.4.0", - "PyMySQL==1.0.2", + "PyMySQL==1.0.3", "PyPDF2~=2.1.0", "PyPika~=0.48.9", "PyQRCode~=1.2.1", From 6bda014406addb89aac3a17c68c997abf01b9db7 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Tue, 28 Mar 2023 05:04:45 -0400 Subject: [PATCH 079/120] fix: install cypress plugins in frappe namespace (#20459) --- frappe/commands/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 01de67eb05..03374986d4 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -912,7 +912,7 @@ def run_ui_tests( os.chdir(app_base_path) - node_bin = subprocess.getoutput("yarn bin") + node_bin = subprocess.getoutput("(cd ../frappe && yarn bin)") cypress_path = f"{node_bin}/cypress" drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop" real_events_plugin_path = f"{node_bin}/../cypress-real-events" @@ -939,7 +939,7 @@ def run_ui_tests( "@cypress/code-coverage@^3", ] ) - frappe.commands.popen(f"yarn add {packages} --no-lockfile") + frappe.commands.popen(f"(cd ../frappe && yarn add {packages} --no-lockfile)") # run for headless mode run_or_open = "run --browser chrome --record" if headless else "open" From e756d417901b755ff2e65de97dddca06d25ee658 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 28 Mar 2023 17:24:20 +0530 Subject: [PATCH 080/120] chore: add insights banner (#20487) * chore: add insights banner * chore: cleanup message and title [skip ci] --------- Co-authored-by: Ankush Menat --- frappe/public/js/frappe/list/list_sidebar.js | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index f845f3a375..fe5f0b810f 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -38,6 +38,8 @@ frappe.views.ListSidebar = class ListSidebar { this.reload_stats(); }); } + + this.add_insights_banner(); } setup_views() { @@ -239,4 +241,43 @@ frappe.views.ListSidebar = class ListSidebar { this.sidebar.find(".stat-no-records").remove(); this.get_stats(); } + + add_insights_banner() { + try { + if (this.list_view.view != "Report") { + return; + } + + if (localStorage.getItem("show_insights_banner") == "false") { + return; + } + + if (this.insights_banner) { + this.insights_banner.remove(); + } + + const message = "Get more insights from your data with Frappe Insights."; + const link = "https://frappe.io/s/insights"; + const cta = "Get Frappe Insights"; + + this.insights_banner = $(` +
+
+ ${message} +
+ +
+ + + +
+
+ `).appendTo(this.sidebar); + } catch (error) { + console.error(error); + } + } }; From 63338a3806281a2933fac7fb158b730c1c64bca9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 28 Mar 2023 17:33:05 +0530 Subject: [PATCH 081/120] fix: date field shouldn't be formatted for TZ (#20486) Date fields aren't timezone aware. --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 1d555c3603..14f01b6607 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -186,7 +186,7 @@ frappe.form.formatters = { return value; } if (value) { - value = frappe.datetime.str_to_user(value); + value = frappe.datetime.str_to_user(value, false, true); // handle invalid date if (value === "Invalid date") { value = null; From 90f8f945b4f830d458ad453f81fc95fa3dfe9f75 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 28 Mar 2023 18:13:37 +0530 Subject: [PATCH 082/120] feat: Disable Sharing globally (#20318) * feat: Disable Sharing globally - Checkbox in System Settings - If disabled, avoid share UI render - Share APIs return None (non-obstructing) if share APIs are invoked * feat: Settings checkbox must toggle share permission globally - Treat feature like a perm toggler. Essentially noone is allowed to explicity share anything - Implicit sharing via `ignore_share_permissions` is allowed. Devs can decide where sharing should happen under the hood - UI is made read only and not hidden. Users must see who doc is already shared with - Make sure perm APIs used by share feature return false if sharing is disabled - Rename checkbox to `Disable Document Sharing` * test: (server side) Impact of disabling sharing on APIs - Also, fix missed system setting rename in `assign_to` * fix: Inform assigner if assignee lacks perms and sharing is disabled - misc: readable conditions * fix: throw instead of msgprint * fix: Typo and appropriate message for `throw` --------- Co-authored-by: Ankush Menat --- frappe/core/doctype/docshare/test_docshare.py | 65 ++++++++++++++++++- .../system_settings/system_settings.json | 11 +++- frappe/desk/form/assign_to.py | 13 +++- frappe/permissions.py | 3 + frappe/public/js/frappe/model/model.js | 6 ++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py index b874042d15..5a2a8da936 100644 --- a/frappe/core/doctype/docshare/test_docshare.py +++ b/frappe/core/doctype/docshare/test_docshare.py @@ -4,7 +4,7 @@ import frappe import frappe.share from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings test_dependencies = ["User"] @@ -139,3 +139,66 @@ class TestDocShare(FrappeTestCase): test_doc.reload() self.assertTrue(test_doc.has_permission("read")) + + @change_settings("System Settings", {"disable_document_sharing": 1}) + def test_share_disabled_add(self): + "Test if user loses share access on disabling share globally." + frappe.share.add("Event", self.event.name, self.user, share=1) # Share as admin + frappe.set_user(self.user) + + # User does not have share access although given to them + self.assertFalse(self.event.has_permission("share")) + self.assertRaises( + frappe.PermissionError, frappe.share.add, "Event", self.event.name, "test1@example.com" + ) + + @change_settings("System Settings", {"disable_document_sharing": 1}) + def test_share_disabled_add_with_ignore_permissions(self): + frappe.share.add("Event", self.event.name, self.user, share=1) + frappe.set_user(self.user) + + # User does not have share access although given to them + self.assertFalse(self.event.has_permission("share")) + + # Test if behaviour is consistent for developer overrides + frappe.share.add_docshare( + "Event", self.event.name, "test1@example.com", flags={"ignore_share_permission": True} + ) + + @change_settings("System Settings", {"disable_document_sharing": 1}) + def test_share_disabled_set_permission(self): + frappe.share.add("Event", self.event.name, self.user, share=1) + frappe.set_user(self.user) + + # User does not have share access although given to them + self.assertFalse(self.event.has_permission("share")) + self.assertRaises( + frappe.PermissionError, + frappe.share.set_permission, + "Event", + self.event.name, + "test1@example.com", + "read", + ) + + @change_settings("System Settings", {"disable_document_sharing": 1}) + def test_share_disabled_assign_to(self): + """ + Assigning a document to a user without access must not share the document, + if sharing disabled. + """ + from frappe.desk.form.assign_to import add, get + + frappe.share.add("Event", self.event.name, self.user, share=1) + frappe.set_user(self.user) + + # Assign to 'test1@example.com' + add({"doctype": "Event", "name": self.event.name, "assign_to": ["test1@example.com"]}) + + # Check if assigned to 'test1@example.com' + assignments = get(dict(doctype="Event", name=self.event.name)) + self.assertEqual(len(assignments), 1) + + # Check if not shared with 'test1@example.com' + shared_users = [x.user for x in frappe.share.get_users("Event", self.event.name)] + self.assertNotIn("test1@example.com", shared_users) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 8ebcb493de..102a0a76c2 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -13,6 +13,7 @@ "time_zone", "enable_onboarding", "setup_complete", + "disable_document_sharing", "date_and_number_format", "date_format", "time_format", @@ -528,12 +529,18 @@ "fieldtype": "Select", "label": "Rounding Method", "options": "Banker's Rounding (legacy)\nBanker's Rounding\nCommercial Rounding" + }, + { + "default": "0", + "fieldname": "disable_document_sharing", + "fieldtype": "Check", + "label": "Disable Document Sharing" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-03-10 12:23:45.248125", + "modified": "2023-03-14 11:30:56.465653", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -552,4 +559,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index 72265dce1f..2b17d38371 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -93,10 +93,17 @@ def add(args=None): doc = frappe.get_doc(args["doctype"], args["name"]) - # if assignee does not have permissions, share + # if assignee does not have permissions, share or inform if not frappe.has_permission(doc=doc, user=assign_to): - frappe.share.add(doc.doctype, doc.name, assign_to) - shared_with_users.append(assign_to) + if frappe.get_system_settings("disable_document_sharing"): + msg = _("User {0} is not permitted to access this document.").format(frappe.bold(assign_to)) + msg += "
" + _( + "As document sharing is disabled, please give them the required permissions before assigning." + ) + frappe.throw(msg, title=_("Missing Permission")) + else: + frappe.share.add(doc.doctype, doc.name, assign_to) + shared_with_users.append(assign_to) # make this document followed by assigned user if frappe.get_cached_value("User", assign_to, "follow_assigned_documents"): diff --git a/frappe/permissions.py b/frappe/permissions.py index 75a940233e..97badea500 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -77,6 +77,9 @@ def has_permission( if user == "Administrator": return True + if ptype == "share" and frappe.get_system_settings("disable_document_sharing"): + return False + if not doc and hasattr(doctype, "doctype"): # first argument can be doc or doctype doc = doctype diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index ebf1ff3eac..6c6f617275 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -447,6 +447,12 @@ $.extend(frappe.model, { }, can_share: function (doctype, frm) { + let disable_sharing = cint(frappe.sys_defaults.disable_document_sharing); + + if (disable_sharing && frappe.session.user !== "Administrator") { + return false; + } + if (frm) { return frm.perm[0].share === 1; } From 513a209d531ecccc0f143a8cd13376e51032a6fa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 29 Mar 2023 09:25:20 +0530 Subject: [PATCH 083/120] test: fix test case to modified behaviour We throw instead of showing warning now --- frappe/core/doctype/docshare/test_docshare.py | 17 ++++++----------- frappe/tests/utils.py | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py index 5a2a8da936..e080b0d4ff 100644 --- a/frappe/core/doctype/docshare/test_docshare.py +++ b/frappe/core/doctype/docshare/test_docshare.py @@ -187,18 +187,13 @@ class TestDocShare(FrappeTestCase): Assigning a document to a user without access must not share the document, if sharing disabled. """ - from frappe.desk.form.assign_to import add, get + from frappe.desk.form.assign_to import add frappe.share.add("Event", self.event.name, self.user, share=1) frappe.set_user(self.user) - # Assign to 'test1@example.com' - add({"doctype": "Event", "name": self.event.name, "assign_to": ["test1@example.com"]}) - - # Check if assigned to 'test1@example.com' - assignments = get(dict(doctype="Event", name=self.event.name)) - self.assertEqual(len(assignments), 1) - - # Check if not shared with 'test1@example.com' - shared_users = [x.user for x in frappe.share.get_users("Event", self.event.name)] - self.assertNotIn("test1@example.com", shared_users) + self.assertRaises( + frappe.ValidationError, + add, + {"doctype": "Event", "name": self.event.name, "assign_to": ["test1@example.com"]}, + ) diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index fe95960518..2cdcfb5643 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -143,7 +143,7 @@ def change_settings(doctype, settings_dict): # change setting for key, value in settings_dict.items(): setattr(settings, key, value) - settings.save() + settings.save(ignore_permissions=True) # singles are cached by default, clear to avoid flake frappe.db.value_cache[settings] = {} yield # yield control to calling function @@ -153,7 +153,7 @@ def change_settings(doctype, settings_dict): settings = frappe.get_doc(doctype) for key, value in previous_settings.items(): setattr(settings, key, value) - settings.save() + settings.save(ignore_permissions=True) def timeout(seconds=30, error_message="Test timed out."): From 6096e45b36108364e7a155501a5478aa3f894751 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Mar 2023 07:01:01 +0200 Subject: [PATCH 084/120] test: switch tests to supported methods (#20494) * fix: switch tests to supported methods get_fetch_fields, update_linked_doctypes * test: semantic assertions * test: fixup deprecation tests imports --------- Co-authored-by: Ankush Menat --- frappe/tests/test_rename_doc.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index e48f908147..3f67bc4a1f 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -9,14 +9,9 @@ from unittest.mock import patch import frappe from frappe.core.doctype.doctype.test_doctype import new_doctype -from frappe.exceptions import DoesNotExistError, ValidationError +from frappe.exceptions import DoesNotExistError from frappe.model.base_document import get_controller -from frappe.model.rename_doc import ( - bulk_rename, - get_fetch_fields, - update_document_title, - update_linked_doctypes, -) +from frappe.model.rename_doc import bulk_rename, update_document_title from frappe.modules.utils import get_doc_path from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, now @@ -255,14 +250,16 @@ class TestRenameDoc(FrappeTestCase): ) def test_deprecated_utils(self): + from frappe.model.rename_doc import get_fetch_fields, update_linked_doctypes + stdout = StringIO() with redirect_stdout(stdout), patch_db(["set_value"]): get_fetch_fields("User", "ToDo", ["Activity Log"]) - self.assertTrue("Function frappe.model.rename_doc.get_fetch_fields" in stdout.getvalue()) + self.assertIn("Function frappe.model.rename_doc.get_fetch_fields", stdout.getvalue()) update_linked_doctypes("User", "ToDo", "str", "str") - self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue()) + self.assertIn("Function frappe.model.rename_doc.update_linked_doctypes", stdout.getvalue()) def test_doc_rename_method(self): name = choice(self.available_documents) From 45c86e2ff843f4dfdd35666c224f3b19845fbd8b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 29 Mar 2023 12:49:28 +0530 Subject: [PATCH 085/120] fix: nestedset rename (#20498) --- frappe/model/rename_doc.py | 13 ++++++++----- frappe/tests/test_nestedset.py | 7 +++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 420cbee091..3908365291 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -488,6 +488,9 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: if frappe.conf.developer_mode: for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"): + if name in (old, new): + continue + doctype = frappe.get_doc("DocType", name) save = False for f in doctype.fields: @@ -496,11 +499,11 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: save = True if save: doctype.save() - else: - DocField = frappe.qb.DocType("DocField") - frappe.qb.update(DocField).set(DocField.options, new).where( - (DocField.fieldtype == fieldtype) & (DocField.options == old) - ).run() + + DocField = frappe.qb.DocType("DocField") + frappe.qb.update(DocField).set(DocField.options, new).where( + (DocField.fieldtype == fieldtype) & (DocField.options == old) + ).run() frappe.qb.update(CustomField).set(CustomField.options, new).where( (CustomField.fieldtype == fieldtype) & (CustomField.options == old) diff --git a/frappe/tests/test_nestedset.py b/frappe/tests/test_nestedset.py index 182831b680..ef63fb66c2 100644 --- a/frappe/tests/test_nestedset.py +++ b/frappe/tests/test_nestedset.py @@ -8,6 +8,7 @@ from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.query_builder import Field from frappe.query_builder.functions import Max from frappe.tests.utils import FrappeTestCase +from frappe.utils import random_string from frappe.utils.nestedset import ( NestedSetChildExistsError, NestedSetInvalidMergeError, @@ -213,6 +214,12 @@ class TestNestedSet(FrappeTestCase): remove_subtree("Test Tree DocType", "Parent 2") self.test_basic_tree() + def test_rename_nestedset(self): + doctype = new_doctype(is_tree=True).insert() + + # Rename doctype + frappe.rename_doc("DocType", doctype.name, "Test " + random_string(10), force=True) + def test_merge_groups(self): global records el = {"some_fieldname": "Parent 2", "parent_test_tree_doctype": "Root Node", "is_group": 1} From 40ad98359896bfdbe263fc7238334ad7e11e023f Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Mar 2023 09:31:32 +0200 Subject: [PATCH 086/120] feat(contact template): clickable email ids and phone numbers, less labels (#20247) * refactor(contact template): use for .. of loop * refactor(contact template): clickable phone numbers and email ids * refactor(contact template): less labels * fix: escape phone and email ids --- .../frappe/form/templates/contact_list.html | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/frappe/public/js/frappe/form/templates/contact_list.html b/frappe/public/js/frappe/form/templates/contact_list.html index e5fdc3f88e..c4cc08a549 100644 --- a/frappe/public/js/frappe/form/templates/contact_list.html +++ b/frappe/public/js/frappe/form/templates/contact_list.html @@ -1,49 +1,51 @@
-{% for(var i=0, l=contact_list.length; i

- {%= contact_list[i].first_name %} {%= contact_list[i].last_name %} - {% if(contact_list[i].is_primary_contact) { %} + {%= contact.first_name %} {%= contact.last_name %} + {% if(contact.is_primary_contact) { %}  ({%= __("Primary") %}) {% } %} - {% if(contact_list[i].designation){ %} - – {%= contact_list[i].designation %} + {% if(contact.designation){ %} + – {%= contact.designation %} {% } %} - {%= __("Edit") %}

- {% if (contact_list[i].phones || contact_list[i].email_ids) { %} + {% if (contact.phone || contact.mobile_no || contact.phone_nos.length > 0) { %}

- {% if(contact_list[i].phone) { %} - {%= __("Phone") %}: {%= contact_list[i].phone %} ({%= __("Primary") %})
+ {% if(contact.phone) { %} + {%= frappe.utils.escape_html(contact.phone) %} · {%= __("Primary Phone") %}
{% endif %} - {% if(contact_list[i].mobile_no) { %} - {%= __("Mobile No") %}: {%= contact_list[i].mobile_no %} ({%= __("Primary") %})
+ {% if(contact.mobile_no) { %} + {%= frappe.utils.escape_html(contact.mobile_no) %} · {%= __("Primary Mobile") %}
{% endif %} - {% if(contact_list[i].phone_nos) { %} - {% for(var j=0, k=contact_list[i].phone_nos.length; j - {% } %} - {% endif %} -

-

- {% if(contact_list[i].email_id) { %} - {%= __("Email") %}: {%= contact_list[i].email_id %} ({%= __("Primary") %})
- {% endif %} - {% if(contact_list[i].email_ids) { %} - {% for(var j=0, k=contact_list[i].email_ids.length; j + {% if(contact.phone_nos) { %} + {% for(const phone_no of contact.phone_nos) { %} + {%= frappe.utils.escape_html(phone_no.phone) %}
{% } %} {% endif %}

{% endif %} + {% if (contact.email_id || contact.email_ids.length > 0) { %}

- {% if (contact_list[i].address) { %} - {%= __("Address") %}: {%= contact_list[i].address %}
- {% endif %} + {% if(contact.email_id) { %} + {%= frappe.utils.escape_html(contact.email_id) %} · {%= __("Primary Email") %}
+ {% endif %} + {% if(contact.email_ids) { %} + {% for(const email_id of contact.email_ids) { %} + {%= frappe.utils.escape_html(email_id.email_id) %}
+ {% } %} + {% endif %}

+ {% endif %} + {% if (contact.address) { %} +

+ {%= contact.address %} +

+ {% endif %} {% } %} {% if(!contact_list.length) { %} @@ -51,4 +53,4 @@ {% } %}

-

\ No newline at end of file +

From a56ea73b7d048c1536bc9957ba6e9cde2d1ddc3f Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 30 Mar 2023 11:52:53 +0530 Subject: [PATCH 087/120] fix: escape HTML instead of sanitizing --- frappe/www/printview.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 9fdc77a2ba..38a0409e5f 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -11,7 +11,7 @@ import frappe from frappe import _, get_module_path from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.document_share_key.document_share_key import is_expired -from frappe.utils import cint, sanitize_html, strip_html +from frappe.utils import cint, escape_html, strip_html from frappe.utils.jinja_globals import is_rtl if TYPE_CHECKING: @@ -27,12 +27,11 @@ def get_context(context): """Build context for print""" if not ((frappe.form_dict.doctype and frappe.form_dict.name) or frappe.form_dict.doc): return { - "body": sanitize_html( - """

Error

+ "body": f""" +

Error

Parameters doctype and name required

-
%s
""" - % repr(frappe.form_dict) - ) +
{escape_html(frappe.as_json(frappe.form_dict, indent=2))}
+ """ } if frappe.form_dict.doc: From 9758781f809a60fb1f1c3c29ad746f98478d6a3d Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 30 Mar 2023 13:40:44 +0200 Subject: [PATCH 088/120] fix: bulk update using doc method, check perms (#20522) * fix: bulk update * fix: always check permission --- .../desk/doctype/bulk_update/bulk_update.js | 49 +++++++------------ .../desk/doctype/bulk_update/bulk_update.py | 32 ++++++------ 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/frappe/desk/doctype/bulk_update/bulk_update.js b/frappe/desk/doctype/bulk_update/bulk_update.js index 017eee1480..d8a2b89cf3 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.js +++ b/frappe/desk/doctype/bulk_update/bulk_update.js @@ -16,38 +16,27 @@ frappe.ui.form.on("Bulk Update", { if (!frm.doc.update_value) { frappe.throw(__('Field "value" is mandatory. Please specify value to be updated')); } else { - frappe - .call({ - method: "frappe.desk.doctype.bulk_update.bulk_update.update", - args: { - doctype: frm.doc.document_type, - field: frm.doc.field, - value: frm.doc.update_value, - condition: frm.doc.condition, - limit: frm.doc.limit, - }, - }) - .then((r) => { - let failed = r.message; - if (!failed) failed = []; + frm.call("bulk_update").then((r) => { + let failed = r.message; + if (!failed) failed = []; - if (failed.length && !r._server_messages) { - frappe.throw( - __("Cannot update {0}", [ - failed.map((f) => (f.bold ? f.bold() : f)).join(", "), - ]) - ); - } else { - frappe.msgprint({ - title: __("Success"), - message: __("Updated Successfully"), - indicator: "green", - }); - } + if (failed.length && !r._server_messages) { + frappe.throw( + __("Cannot update {0}", [ + failed.map((f) => (f.bold ? f.bold() : f)).join(", "), + ]) + ); + } else { + frappe.msgprint({ + title: __("Success"), + message: __("Updated Successfully"), + indicator: "green", + }); + } - frappe.hide_progress(); - frm.save(); - }); + frappe.hide_progress(); + frm.save(); + }); } }); }, diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 5521d9583f..535be8155f 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -10,26 +10,24 @@ from frappe.utils.scheduler import is_scheduler_inactive class BulkUpdate(Document): - pass + @frappe.whitelist() + def bulk_update(self): + self.check_permission("write") + limit = self.limit if self.limit and cint(self.limit) < 500 else 500 + condition = "" + if self.condition: + if ";" in self.condition: + frappe.throw(_("; not allowed in condition")) -@frappe.whitelist() -def update(doctype, field, value, condition="", limit=500): - if not limit or cint(limit) > 500: - limit = 500 + condition = f" where {self.condition}" - if condition: - condition = " where " + condition - - if ";" in condition: - frappe.throw(_("; not allowed in condition")) - - docnames = frappe.db.sql_list( - f"""select name from `tab{doctype}`{condition} limit {limit} offset 0""" - ) - data = {} - data[field] = value - return submit_cancel_or_update_docs(doctype, docnames, "update", data) + docnames = frappe.db.sql_list( + f"""select name from `tab{self.document_type}`{condition} limit {limit} offset 0""" + ) + return submit_cancel_or_update_docs( + self.document_type, docnames, "update", {self.field: self.update_value} + ) @frappe.whitelist() From 2cb492561a2ba790241b081be507b252d8061fac Mon Sep 17 00:00:00 2001 From: Himanshu Shivhare Date: Thu, 30 Mar 2023 17:57:00 +0530 Subject: [PATCH 089/120] fix(ux): correct email account setup path in error message (#20513) --- frappe/core/doctype/communication/email.py | 2 +- frappe/email/doctype/email_account/email_account.py | 2 +- frappe/email/smtp.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 2e199e014d..1733b7b716 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -165,7 +165,7 @@ def _make( if not comm.get_outgoing_email_account(): frappe.throw( _( - "Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account" + "Unable to send mail because of a missing email account. Please setup default Email Account from Settings > Email Account" ), exc=frappe.OutgoingEmailError, ) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 2e5dbe2e24..faf28afdb3 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -325,7 +325,7 @@ class EmailAccount(Document): if _raise_error: frappe.throw( - _("Please setup default Email Account from Setup > Email > Email Account"), + _("Please setup default Email Account from Settings > Email Account"), frappe.OutgoingEmailError, ) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 028b21b0ae..5e1b5ef296 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -70,7 +70,7 @@ class SMTPServer: if not self.server: frappe.msgprint( _( - "Email Account not setup. Please create a new Email Account from Setup > Email > Email Account" + "Email Account not setup. Please create a new Email Account from Settings > Email Account" ), raise_exception=frappe.OutgoingEmailError, ) From aab37e0a6ca59727edf7c48ee9bc64d09677fcea Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Fri, 31 Mar 2023 13:31:18 +0530 Subject: [PATCH 090/120] fix: Check if reference_name is set --- frappe/core/doctype/communication/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 22b7e8a0fc..52ea93d829 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -217,7 +217,7 @@ class CommunicationEmailMixin: "reference_type": self.reference_doctype, } - if self.reference_doctype == "ToDo" and self.reference_name != None: + if self.reference_doctype and self.reference_name: return ToDo.get_owners(filters) else: return [] From c509983ca4d4450609e6960b858148635bf54950 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 31 Mar 2023 13:37:55 +0530 Subject: [PATCH 091/120] build: bump redis version https://github.com/redis/redis-py/releases --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 48903f3163..daa0748e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ "python-dateutil~=2.8.1", "pytz==2022.1", "rauth~=0.7.3", - "redis~=4.3.4", + "redis~=4.5.4", "hiredis~=2.0.0", "requests-oauthlib~=1.3.0", "requests~=2.27.1", From 7a92a604e0686e1679c6df3ecce7a9f38e0c1506 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Fri, 31 Mar 2023 14:03:49 +0530 Subject: [PATCH 092/120] style: Fix formatting --- frappe/email/smtp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 5e1b5ef296..3b22bc4ce4 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -69,9 +69,7 @@ class SMTPServer: if not self.server: frappe.msgprint( - _( - "Email Account not setup. Please create a new Email Account from Settings > Email Account" - ), + _("Email Account not setup. Please create a new Email Account from Settings > Email Account"), raise_exception=frappe.OutgoingEmailError, ) From 5ad9350b14ada372ed4a528d3988178218c87ebc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 31 Mar 2023 14:37:51 +0530 Subject: [PATCH 093/120] fix: Handle JsBarcode exceptions (#20533) JsBarcode exceptions prevent entire page from loading. Instead of that catch error and show it in helpbox so user can correct the barcode if required. Steps to reproduce: 1. Add barcode field 2. Set barcode type in options 3. add invalid barcode and save (cherry picked from commit 57d40b2614068c13b4b14f42438b46b22c0f5533) --- frappe/public/js/frappe/form/controls/barcode.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js index c130ecc039..a819384773 100644 --- a/frappe/public/js/frappe/form/controls/barcode.js +++ b/frappe/public/js/frappe/form/controls/barcode.js @@ -27,6 +27,7 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont let svg = value; let barcode_value = ""; + this.set_empty_description(); if (value && value.startsWith(" Date: Fri, 31 Mar 2023 17:42:13 +0530 Subject: [PATCH 094/120] perf: Faster address query with explicit joins This querry is particularly problamatic when there are OR conditions due to shared docs. ```sql ANALYZE SELECT `tabAddress`.name, `tabAddress`.city, `tabAddress`.country FROM `tabAddress` join `tabDynamic Link` on (`tabDynamic Link`.parent = `tabAddress`.name AND `tabDynamic Link`.parenttype = 'Address') WHERE `tabDynamic Link`.link_doctype = 'Customer' AND `tabDynamic Link`.link_name = 'CUSTOMER NAME' AND coalesce(`tabAddress`.disabled, 0) = 0 AND (`tabAddress`.`country` like '%%' OR `tabAddress`.`state` like '%%' OR `tabAddress`.`name` like '%%' OR `tabAddress`.`name` like '%%') AND ((((coalesce(`tabAddress`.`branch`, '')='' OR `tabAddress`.`branch` in ('something'))))) OR (`tabAddress`.name in ('SOME SHARED DOC')) ORDER BY if(locate('', `tabAddress`.name), locate('', `tabAddress`.name), 99999), `tabAddress`.idx DESC, `tabAddress`.name LIMIT 0, 20; ``` **Before:** Never terminates and reads entire table with millions of lines :LOL: **After:** Reads ~100 rows max. ``` *************************** 1. row *************************** id: 1 select_type: SIMPLE table: tabDynamic Link type: index_merge possible_keys: parent,link_doctype_link_name_index,link_name key: link_name,link_doctype_link_name_index,parent key_len: 563,1126,563 ref: NULL rows: 40 r_rows: 79.00 filtered: 100.00 r_filtered: 100.00 Extra: Using union(intersect(link_name,link_doctype_link_name_index),parent); Using where; Using temporary; Using filesort *************************** 2. row *************************** id: 1 select_type: SIMPLE table: tabAddress type: eq_ref possible_keys: PRIMARY,selco_branch key: PRIMARY key_len: 562 ref: _900a733bdc3bb9ed.tabDynamic Link.parent rows: 1 r_rows: 1.00 filtered: 100.00 r_filtered: 100.00 Extra: Using where 2 rows in set (0.001 sec) ``` > Please provide enough information so that others can review your pull request: > Explain the **details** for making this change. What existing problem does the pull request solve? > Screenshots/GIFs --- frappe/contacts/doctype/address/address.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 5fe22eb7f2..77dee10615 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -254,10 +254,10 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): """select `tabAddress`.name, `tabAddress`.city, `tabAddress`.country from - `tabAddress`, `tabDynamic Link` + `tabAddress` + join `tabDynamic Link` + on (`tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address') where - `tabDynamic Link`.parent = `tabAddress`.name and - `tabDynamic Link`.parenttype = 'Address' and `tabDynamic Link`.link_doctype = %(link_doctype)s and `tabDynamic Link`.link_name = %(link_name)s and ifnull(`tabAddress`.disabled, 0) = 0 and From fba3497ba0f2c549453d6cae3c6cc553494a7a22 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 31 Mar 2023 19:04:11 +0530 Subject: [PATCH 095/120] test: address query --- .../contacts/doctype/address/test_address.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frappe/contacts/doctype/address/test_address.py b/frappe/contacts/doctype/address/test_address.py index 1d11c5efef..d30b0f9f78 100644 --- a/frappe/contacts/doctype/address/test_address.py +++ b/frappe/contacts/doctype/address/test_address.py @@ -1,7 +1,9 @@ # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE +from functools import partial + import frappe -from frappe.contacts.doctype.address.address import get_address_display +from frappe.contacts.doctype.address.address import address_query, get_address_display from frappe.tests.utils import FrappeTestCase @@ -28,3 +30,29 @@ class TestAddress(FrappeTestCase): address = frappe.get_list("Address")[0].name display = get_address_display(frappe.get_doc("Address", address).as_dict()) self.assertTrue(display) + + def test_address_query(self): + def query(doctype="Address", txt="", searchfield="name", start=0, page_len=20, filters=None): + if filters is None: + filters = {"link_doctype": "User", "link_name": "Administrator"} + return address_query(doctype, txt, searchfield, start, page_len, filters) + + frappe.get_doc( + { + "address_type": "Billing", + "address_line1": "1", + "city": "Mumbai", + "state": "Maharashtra", + "country": "India", + "doctype": "Address", + "links": [ + { + "link_doctype": "User", + "link_name": "Administrator", + } + ], + } + ).insert() + + self.assertGreaterEqual(len(query(txt="admin")), 1) + self.assertEqual(len(query(txt="what_zyx")), 0) From ae3b3ebb174d4709f01f002014ebaa0930632aab Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sat, 1 Apr 2023 18:52:03 +0200 Subject: [PATCH 096/120] chore: remove excessive whitespace (#20544) to make the linter happy [skip ci] --- frappe/core/doctype/communication/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 52ea93d829..24b6a8fafb 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -216,7 +216,7 @@ class CommunicationEmailMixin: "reference_name": self.reference_name, "reference_type": self.reference_doctype, } - + if self.reference_doctype and self.reference_name: return ToDo.get_owners(filters) else: From b72ec114eeed6de1bc77cedc9b5cc540ae3dd769 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Sun, 2 Apr 2023 14:33:35 +0530 Subject: [PATCH 097/120] fix(UI): align link cards & charts on workspace --- frappe/public/scss/desk/desktop.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 41eaa4777d..cef6e2e52a 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -208,7 +208,6 @@ body { // Overrides for each widgets &.dashboard-widget-box { min-height: 240px; - padding: var(--padding-md) var(--padding-lg); .filter-chart { background-color: var(--control-bg); @@ -238,13 +237,16 @@ body { } .widget-head { - padding: var(--padding-sm); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 6px; } + .widget-body { + padding-top: 7px; + } + .widget-control { display: flex; align-items: center; @@ -560,7 +562,7 @@ body { } &.links-widget-box { - padding: 18px 12px; + padding: 12px 7px; .link-item { display: flex; From 10dcbc5ecf2aa22b128a71a52061eb3a1c3da5f7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 2 Apr 2023 15:10:29 +0530 Subject: [PATCH 098/120] fix: fix address query for postgres refer https://github.com/frappe/frappe/pull/20537#ref-pullrequest-1645575433 --- frappe/contacts/doctype/address/address.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 77dee10615..a1ecbdd4a5 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -266,7 +266,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): order by if(locate(%(_txt)s, `tabAddress`.name), locate(%(_txt)s, `tabAddress`.name), 99999), `tabAddress`.idx desc, `tabAddress`.name - limit %(start)s, %(page_len)s """.format( + limit %(page_len)s offset %(start)s""".format( mcond=get_match_cond(doctype), search_condition=search_condition, condition=condition or "", From f206b1582e31108efdd90893270b57ab259aace5 Mon Sep 17 00:00:00 2001 From: Marica Date: Sun, 2 Apr 2023 15:14:40 +0530 Subject: [PATCH 099/120] test: Kanban Test fails to remove System Manager role (#20505) * fix: Kanban Test fails to remove System Manager role - As there's only one user with System Manager role, role removal is reverted - Add another user with this role, so that role removal on test user works * chore: Reuse `create_test_user` util * fix: user switching in kanban test --------- Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- cypress/integration/kanban.js | 19 +++++++++++-------- frappe/tests/ui_test_helpers.py | 9 ++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index 04a72a9436..f14c991c7c 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -98,15 +98,17 @@ context("Kanban Board", () => { }); it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => { - // create admin kanban board - cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" }); - cy.switch_to_user("Administrator"); - cy.call("frappe.tests.ui_test_helpers.create_admin_kanban"); - // remove sys manager - cy.remove_role("frappe@example.com", "System Manager"); - cy.switch_to_user("frappe@example.com"); + const noSystemManager = "nosysmanager@example.com"; + cy.call("frappe.tests.ui_test_helpers.create_test_user", { + username: noSystemManager, + }); + cy.remove_role(noSystemManager, "System Manager"); + cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" }); + cy.call("frappe.tests.ui_test_helpers.create_admin_kanban"); + + cy.switch_to_user(noSystemManager); cy.visit("/app/todo/view/kanban/Admin Kanban"); @@ -122,7 +124,8 @@ context("Kanban Board", () => { // Column actions should be hidden (dropdown for 'Archive' and indicators) cy.get(".kanban .column-options").should("have.length", 0); - cy.add_role("frappe@example.com", "System Manager"); + cy.switch_to_user("Administrator"); + cy.call("frappe.client.delete", { doctype: "User", name: noSystemManager }); }); after(() => { diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 2846b33eb9..4d3e3ec11f 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -426,12 +426,15 @@ def create_blog_post(): return doc -def create_test_user(): - if frappe.db.exists("User", UI_TEST_USER): +@whitelist_for_tests +def create_test_user(username=None): + name = username or UI_TEST_USER + + if frappe.db.exists("User", name): return user = frappe.new_doc("User") - user.email = UI_TEST_USER + user.email = name user.first_name = "Frappe" user.new_password = frappe.local.conf.admin_password user.send_welcome_email = 0 From 5fff6698ada2d313493f6e50d4c2acfc889c9e8e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 2 Apr 2023 15:23:22 +0530 Subject: [PATCH 100/120] fix: use `develop` as branch name for new apps dont ask me why --- frappe/tests/test_boilerplate.py | 4 ++++ frappe/utils/boilerplate.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py index 999c74592e..0f58e84df4 100644 --- a/frappe/tests/test_boilerplate.py +++ b/frappe/tests/test_boilerplate.py @@ -8,6 +8,7 @@ import unittest from io import StringIO from unittest.mock import patch +import git import yaml import frappe @@ -134,6 +135,9 @@ class TestBoilerPlate(unittest.TestCase): self.check_parsable_python_files(new_app_dir) + app_repo = git.Repo(new_app_dir) + self.assertEqual(app_repo.active_branch.name, "develop") + def test_create_app_without_git_init(self): app_name = "test_app_no_git" diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 1cd57f4695..2e8a5088ed 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -150,7 +150,7 @@ def _create_app_boilerplate(dest, hooks, no_git=False): 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.Repo.init(app_directory, initial_branch="develop") app_repo.git.add(A=True) app_repo.index.commit("feat: Initialize App") From ee9cb43835ee4f4cd14a12784e404a682500e6c7 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Sun, 2 Apr 2023 15:33:43 +0530 Subject: [PATCH 101/120] fix: get workflow_state_fieldname instead of setting workflow_state to none --- frappe/core/doctype/deleted_document/deleted_document.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py index 3d4afa40ab..9aa8e41708 100644 --- a/frappe/core/doctype/deleted_document/deleted_document.py +++ b/frappe/core/doctype/deleted_document/deleted_document.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.desk.doctype.bulk_update.bulk_update import show_progress from frappe.model.document import Document +from frappe.model.workflow import get_workflow_name class DeletedDocument(Document): @@ -27,8 +28,11 @@ def restore(name, alert=True): except frappe.DocstatusTransitionError: frappe.msgprint(_("Cancelled Document restored as Draft")) doc.docstatus = 0 - if doc.workflow_state: - doc.workflow_state = None + active_workflow = get_workflow_name(doc.doctype) + if active_workflow: + workflow_state_fieldname = frappe.get_value("Workflow", active_workflow, "workflow_state_field") + if doc.get(workflow_state_fieldname): + doc.set(workflow_state_fieldname, None) doc.insert() doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name)) From 3d626dd36488433e9dd5a72f0bcfa8b0da54c02c Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Mon, 3 Apr 2023 07:53:12 +0200 Subject: [PATCH 102/120] chore: Update de.csv (#20546) alligned singular and plural. Has effect on DocType naming of DocType Notification! --- frappe/translations/de.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index c60fcbb41e..af8ac7bcb8 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -1720,11 +1720,11 @@ Note: Changing the Page Name will break previous URL to this page.,"Hinweis: Wen Note: Multiple sessions will be allowed in case of mobile device,Hinweis: Mehrere Sitzungen wird im Falle einer mobilen Gerät erlaubt sein, Nothing to show,Nichts anzuzeigen, Nothing to update,Nichts zu aktualisieren, -Notification,Mitteilung, +Notification,Benachrichtigung, Notification Recipient,Benachrichtigungsempfänger, Notification Tones,Benachrichtigungstöne, Notifications,Benachrichtigungen, -Notifications and bulk mails will be sent from this outgoing server.,Hinweise und Massen-E-Mails werden von diesem Postausgangsserver versendet., +Notifications and bulk mails will be sent from this outgoing server.,Benachrichtigungen und Massen-E-Mails werden von diesem Postausgangsserver versendet., Notify Users On Every Login,Benutzer bei jeder Anmeldung benachrichtigen, Notify if unreplied,"Benachrichtigen, wenn unbeantwortet", Notify if unreplied for (in mins),"Benachrichtigen, wenn unbeantwortet für (in Minuten)", From 492fdbeec44d1fa164eb750ba1cd804b2caa7a78 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Mon, 3 Apr 2023 07:53:34 +0200 Subject: [PATCH 103/120] chore: Update de.csv (#20545) added translations related to Navbar-Settings. Cleanup. --- frappe/translations/de.csv | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index af8ac7bcb8..1ef1bf1a9b 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -1383,6 +1383,7 @@ Inverse,Invertieren, Is,Ist, Is Attachments Folder,Ist Ordner für Anhänge, Is Child Table,Ist Untertabelle, +Is Custom,Ist benutzerdefiniert, Is Custom Field,Ist benutzerdefiniertes Feld, Is First Startup,Ist Erstes Startup, Is Folder,Ist Ordner, @@ -1613,6 +1614,7 @@ Naming,Bezeichnung, Naming Rule, Benennungsregel, "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
","Namensoptionen:
  1. Feld: [Feldname] - Nach Feld
  2. naming_series: - Nach der Namensreihe (das Feld naming_series muss vorhanden sein)
  3. Eingabeaufforderung - Benutzer nach einem Namen fragen
  4. [Serie] - Reihe nach Präfix (getrennt durch einen Punkt); zum Beispiel PRE. #####
  5. Format: BEISPIEL- {MM} morewords {Feldname1} - {Feldname2} - {#####} - Ersetzt alle verspannten Wörter (Feldnamen, Datumsworte (DD, MM, YY), Serien) durch ihren Wert. Außerhalb von Klammern können beliebige Zeichen verwendet werden.
", Naming Series mandatory,Nummernkreis zwingend erforderlich, +Navigation Settings,Navigationseinstellungen, Nested set error. Please contact the Administrator.,Schachtelfehler. Bitte den Administrator kontaktieren., New Activity,Neue Aktivität, New Chat,Neuer Chat, @@ -2323,6 +2325,7 @@ Show more details,Weiteres, Show only errors,Zeige nur Fehler, "Show title in browser window as ""Prefix - title""","Diesen Eintrag im Browser-Fenster als ""Präfix - Titel"" anzeigen", Showing only Numeric fields from Report,Nur numerische Felder aus Bericht anzeigen, +Sidebar,Seitenleiste, Sidebar Items,Elemente der Seitenleiste, Sidebar Settings,Sidebar-Einstellungen, Sidebar and Comments,Sidebar und Kommentare, From 62a1f234fe2945cb94ab3ae3d924b1166e434fa5 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Mon, 3 Apr 2023 07:53:49 +0200 Subject: [PATCH 104/120] chore: Update de.csv (#20547) added missing DocType translation. This used to be Naming Series (Nummernkreis) https://doku.phamos.eu/books/erpnext-benutzerhandbuch-v14/page/dokumentenbenennungseinstellungen-document-naming-settings --- frappe/translations/de.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 1ef1bf1a9b..6ef7faa337 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -64,6 +64,7 @@ Delivery Status,Lieferstatus, Department,Abteilung, Details,Details, Document Name,Dokumentenname, +Document Naming Settings,Dokumentenbenennungseinstellungen, Document Status,Dokumentenstatus, Document Type,Dokumententyp, Domain,Domäne, From 75973fd22a6935e862afd48cb23bbec7ebacf199 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Mon, 3 Apr 2023 07:53:59 +0200 Subject: [PATCH 105/120] chore: Update de.csv (#20548) translations for new feature to crop images on upload. Crop (cutting) is in conflict with crop (Agricultural) but as this module is not part of ERPNext-Standard installation. This should not matter for now. --- frappe/translations/de.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 6ef7faa337..2dd903ee93 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -52,6 +52,7 @@ Content,Inhalt, Content Type,Inhaltstyp, Create,Erstellen, Created By,Erstellt von, +Crop,Zuschneiden, Current,Laufend, Custom HTML,Benutzerdefiniertes HTML, Custom?,Benutzerdefiniert?, From a1aaed0a5f23b8a52e0f075ca953e4102679102e Mon Sep 17 00:00:00 2001 From: Sabu Siyad Date: Mon, 3 Apr 2023 11:26:49 +0530 Subject: [PATCH 106/120] feat(util): `get_table_name`: wrap in backticks (#20553) --- frappe/utils/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 5d1aed259a..ef32ff5653 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -1031,8 +1031,13 @@ def groupby_metric(iterable: dict[str, list], key: str): return records -def get_table_name(table_name: str) -> str: - return f"tab{table_name}" if not table_name.startswith("__") else table_name +def get_table_name(table_name: str, wrap_in_backticks: bool = False) -> str: + name = f"tab{table_name}" if not table_name.startswith("__") else table_name + + if wrap_in_backticks: + return f"`{name}`" + + return name def squashify(what): From b443bca2d58c793fe4e5061fded9ff8b31aee10a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 3 Apr 2023 12:37:43 +0530 Subject: [PATCH 107/120] fix: hide chart and heatmap on dashboard reset --- frappe/public/js/frappe/form/dashboard.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 2e3fd9ba99..3c53b8d397 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -69,6 +69,12 @@ frappe.ui.form.Dashboard = class FormDashboard { this.progress_area.body.empty(); this.progress_area.hide(); + // clear heatmap + this.heatmap_area.hide(); + + // clear chart + this.chart_area.hide(); + // clear links this.links_area.body.find(".count, .open-notification").addClass("hidden"); this.links_area.hide(); From fa32b610d661bae0ca935cf3a4f7c3f5c3ecac66 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 3 Apr 2023 14:47:45 +0530 Subject: [PATCH 108/120] fix: rewrite query for postgres (#20557) --- frappe/contacts/doctype/address/address.py | 6 +++++- frappe/contacts/doctype/address/test_address.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index a1ecbdd4a5..965425019c 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -264,7 +264,11 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): ({search_condition}) {mcond} {condition} order by - if(locate(%(_txt)s, `tabAddress`.name), locate(%(_txt)s, `tabAddress`.name), 99999), + case + when locate(%(_txt)s, `tabAddress`.name) != 0 + then locate(%(_txt)s, `tabAddress`.name) + else 99999 + end, `tabAddress`.idx desc, `tabAddress`.name limit %(page_len)s offset %(start)s""".format( mcond=get_match_cond(doctype), diff --git a/frappe/contacts/doctype/address/test_address.py b/frappe/contacts/doctype/address/test_address.py index d30b0f9f78..ecb95f9e0c 100644 --- a/frappe/contacts/doctype/address/test_address.py +++ b/frappe/contacts/doctype/address/test_address.py @@ -54,5 +54,5 @@ class TestAddress(FrappeTestCase): } ).insert() - self.assertGreaterEqual(len(query(txt="admin")), 1) + self.assertGreaterEqual(len(query(txt="Admin")), 1) self.assertEqual(len(query(txt="what_zyx")), 0) From 06580bdbff9a8f86709e52c82afe0cb9da2dc1d4 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Mon, 3 Apr 2023 15:02:05 +0530 Subject: [PATCH 109/120] fix: allow `reset_otp_secret` only if Two Factor Auth is enabled (#20506) * fix: display `Reset OTP Secret` button only if Two factor Auth is enabled * fix: added validations and fetched value from cached doc * fix: linter changes --- frappe/core/doctype/user/user.js | 5 ++++- frappe/twofactor.py | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 413dd07dc4..918a9ee37c 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -219,7 +219,10 @@ frappe.ui.form.on("User", { }); } - if (frappe.session.user == doc.name || frappe.user.has_role("System Manager")) { + if ( + cint(frappe.boot.sysdefaults.enable_two_factor_auth) && + (frappe.session.user == doc.name || frappe.user.has_role("System Manager")) + ) { frm.add_custom_button( __("Reset OTP Secret"), function () { diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 8ad02f0b5a..c4292b0533 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -450,12 +450,20 @@ def disable(): @frappe.whitelist() -def reset_otp_secret(user): +def reset_otp_secret(user: str): if frappe.session.user != user: frappe.only_for("System Manager", message=True) - otp_issuer = frappe.db.get_single_value("System Settings", "otp_issuer_name") - user_email = frappe.db.get_value("User", user, "email") + settings = frappe.get_cached_doc("System Settings") + + if not settings.enable_two_factor_auth: + frappe.throw( + _("You have to enable Two Factor Auth from System Settings."), + title=_("Enable Two Factor Auth"), + ) + + otp_issuer = settings.otp_issuer_name or "Frappe Framework" + user_email = frappe.get_cached_value("User", user, "email") clear_default(user + "_otplogin") clear_default(user + "_otpsecret") @@ -463,10 +471,10 @@ def reset_otp_secret(user): email_args = { "recipients": user_email, "sender": None, - "subject": _("OTP Secret Reset - {0}").format(otp_issuer or "Frappe Framework"), + "subject": _("OTP Secret Reset - {0}").format(otp_issuer), "message": _( "

Your OTP secret on {0} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.

" - ).format(otp_issuer or "Frappe Framework"), + ).format(otp_issuer), "delayed": False, "retry": 3, } From d2bde19b5c967b3980ce5175eb74e70e9e74eac4 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Mon, 3 Apr 2023 15:03:37 +0530 Subject: [PATCH 110/120] fix: removed unnecessary usage of `@frappe.whitelist` (#20503) --- frappe/core/doctype/server_script/server_script.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index dc502e4683..17cddee1e8 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -169,7 +169,6 @@ class ServerScript(Document): return items -@frappe.whitelist() def setup_scheduler_events(script_name, frequency): """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency From 3db1c1aea00cbc79559a73c5cc9638f99ea07cc8 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Mon, 3 Apr 2023 15:04:46 +0530 Subject: [PATCH 111/120] fix: allowed only POST and PUT methods in `rename_doc` (#20504) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 332224f989..9d7befe2d1 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1274,7 +1274,7 @@ def reload_doc( return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions) -@whitelist() +@whitelist(methods=["POST", "PUT"]) def rename_doc( doctype: str, old: str, From e6b09ec771e6fd1146bd95ad4e9fb42ce19c0927 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 3 Apr 2023 15:05:08 +0530 Subject: [PATCH 112/120] fix: heatmap not reset on dashboard refresh --- frappe/public/js/frappe/form/dashboard.js | 36 +++++++++++------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 3c53b8d397..d6cffe45fe 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -484,27 +484,25 @@ frappe.ui.form.Dashboard = class FormDashboard { // heatmap render_heatmap() { - if (!this.heatmap) { - this.heatmap = new frappe.Chart("#heatmap-" + frappe.model.scrub(this.frm.doctype), { - type: "heatmap", - start: new Date(moment().subtract(1, "year").toDate()), - count_label: "interactions", - discreteDomains: 1, - radius: 3, - data: {}, - }); + this.heatmap = new frappe.Chart("#heatmap-" + frappe.model.scrub(this.frm.doctype), { + type: "heatmap", + start: new Date(moment().subtract(1, "year").toDate()), + count_label: "interactions", + discreteDomains: 1, + radius: 3, + data: {}, + }); - // center the heatmap - this.heatmap_area.show(); - this.heatmap_area.body.find("svg").css({ margin: "auto" }); + // center the heatmap + this.heatmap_area.show(); + this.heatmap_area.body.find("svg").css({ margin: "auto" }); - // message - let heatmap_message = this.heatmap_area.body.find(".heatmap-message"); - if (this.data.heatmap_message) { - heatmap_message.removeClass("hidden").html(this.data.heatmap_message); - } else { - heatmap_message.addClass("hidden"); - } + // message + let heatmap_message = this.heatmap_area.body.find(".heatmap-message"); + if (this.data.heatmap_message) { + heatmap_message.removeClass("hidden").html(this.data.heatmap_message); + } else { + heatmap_message.addClass("hidden"); } } From d586d283c56eb971e548ef0326375c8c0b398d96 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 3 Apr 2023 15:12:39 +0530 Subject: [PATCH 113/120] fix: use chart type passed to `render_graph` on form dashboards --- frappe/public/js/frappe/form/dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index d6cffe45fe..2d21b4bb97 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -571,7 +571,7 @@ frappe.ui.form.Dashboard = class FormDashboard { this.chart_area.show(); this.chart_area.body.empty(); $.extend(args, { - type: "line", + type: args.type || "line", colors: args.colors || ["green"], truncateLegends: 1, axisOptions: { From 71c09c1320dd53d87b77c55234562a5b62abefca Mon Sep 17 00:00:00 2001 From: Ponnusamy <95607086+Ponnusamy1-V@users.noreply.github.com> Date: Mon, 3 Apr 2023 16:59:23 +0530 Subject: [PATCH 114/120] feat: list-view click and drag on check box to select multiple rows (#20457) --- cypress/integration/list_view_drag_select.js | 48 ++++++++++++++++++++ frappe/public/js/frappe/list/list_view.js | 31 +++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 cypress/integration/list_view_drag_select.js diff --git a/cypress/integration/list_view_drag_select.js b/cypress/integration/list_view_drag_select.js new file mode 100644 index 0000000000..d481390d89 --- /dev/null +++ b/cypress/integration/list_view_drag_select.js @@ -0,0 +1,48 @@ +context("List View", () => { + before(() => { + cy.login(); + cy.go_to_list("DocType"); + }); + + it("List view check rows on drag", () => { + cy.get(".list-row-checkbox").then(($checkbox) => { + cy.wrap($checkbox).first().trigger("mousedown"); + cy.get(".level.list-row").each(($ele) => { + cy.wrap($ele).trigger("mousemove"); + }); + cy.document().trigger("mouseup"); + }); + + cy.get(".level.list-row .list-row-checkbox").each(($checkbox) => { + cy.wrap($checkbox).should("be.checked"); + }); + }); + + it("Check all rows are checked", () => { + cy.get(".level.list-row .list-row-checkbox") + .its("length") + .then((len) => { + cy.get(".level-item.list-header-meta") + .should("be.visible") + .should("contain.text", `${len} items selected`); + }); + }); + + it("List view uncheck rows on drag", () => { + cy.get(".list-row-checkbox").then(($checkbox) => { + cy.wrap($checkbox).first().trigger("mousedown"); + cy.get(".level.list-row").each(($ele) => { + cy.wrap($ele).trigger("mousemove"); + }); + cy.document().trigger("mouseup"); + }); + + cy.get(".level.list-row .list-row-checkbox").each(($checkbox) => { + cy.wrap($checkbox).should("not.be.checked"); + }); + }); + + it("Check all rows are unchecked", () => { + cy.get(".level-item.list-header-meta").should("not.be.visible"); + }); +}); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index a7cd09d76d..6944c4df7c 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1052,6 +1052,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { setup_events() { this.setup_filterable(); this.setup_list_click(); + this.setup_drag_click(); this.setup_tag_event(); this.setup_new_doc_event(); this.setup_check_events(); @@ -1228,6 +1229,36 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); } + setup_drag_click() { + /* + Click on the check box in the list view and + drag through the rows to select. + + Do it again to unselect. + + If the first click is on checked checkbox, then it will unselect rows on drag, + else if it is unchecked checkbox, it will select rows on drag. + */ + this.dragClick = false; + this.$result.on("mousedown", ".list-row-checkbox", (e) => { + this.dragClick = true; + this.check = !e.target.checked; + }); + $(document).on("mouseup", () => { + this.dragClick = false; + }); + this.$result.on("mousemove", ".level.list-row", (e) => { + if (this.dragClick) { + this.check_row_on_drag(e, this.check); + } + }); + } + + check_row_on_drag(event, check = true) { + $(event.target).find(".list-row-checkbox").prop("checked", check); + this.on_row_checked(); + } + setup_action_handler() { this.$result.on("click", ".btn-action", (e) => { const $button = $(e.currentTarget); From b66c3d9106faa7db94960a9c2e45b380219143ad Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 3 Apr 2023 19:49:56 +0530 Subject: [PATCH 115/120] fix: content_type can be `None` during file upload (#20572) closes https://github.com/frappe/frappe/issues/20571 --- frappe/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/handler.py b/frappe/handler.py index 0a25f329c7..58241c1223 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -198,7 +198,7 @@ def upload_file(): filename = file.filename content_type = guess_type(filename)[0] - if optimize and content_type.startswith("image/"): + if optimize and content_type and content_type.startswith("image/"): args = {"content": content, "content_type": content_type} if frappe.form_dict.max_width: args["max_width"] = int(frappe.form_dict.max_width) From 67de2a34ac94b9fa6199c2525ac97b66549e47f0 Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 21 Mar 2023 20:09:59 +0530 Subject: [PATCH 116/120] fix: contact us email reply --- frappe/templates/includes/contact.js | 5 +--- frappe/www/contact.py | 43 +++++++--------------------- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/frappe/templates/includes/contact.js b/frappe/templates/includes/contact.js index fb0b73ff80..51dc5dc322 100644 --- a/frappe/templates/includes/contact.js +++ b/frappe/templates/includes/contact.js @@ -28,11 +28,8 @@ frappe.ready(function() { sender: email, message: message, callback: function(r) { - if(r.message==="okay") { + if (!r.exc) { frappe.msgprint('{{ _("Thank you for your message") }}'); - } else { - frappe.msgprint('{{ _("There were errors") }}'); - console.log(r.exc); } $(':input').val(''); } diff --git a/frappe/www/contact.py b/frappe/www/contact.py index cf26539ff4..15d639be97 100644 --- a/frappe/www/contact.py +++ b/frappe/www/contact.py @@ -3,7 +3,7 @@ import frappe from frappe import _ -from frappe.utils import now +from frappe.rate_limiter import rate_limit sitemap = 1 @@ -22,38 +22,17 @@ def get_context(context): return out -max_communications_per_hour = 1000 - - @frappe.whitelist(allow_guest=True) -def send_message(subject="Website Query", message="", sender=""): - if not message: - frappe.response["message"] = "Please write something" - return +@rate_limit(limit=1000, seconds=60 * 60, methods=["POST"]) +def send_message(sender, message, subject="Website Query"): + if forward_to_email := frappe.db.get_single_value("Contact Us Settings", "forward_to_email"): + frappe.sendmail(recipients=forward_to_email, reply_to=sender, content=message, subject=subject) - if not sender: - frappe.response["message"] = "Email Address Required" - return - - # guest method, cap max writes per hour - if ( - frappe.db.sql( - """select count(*) from `tabCommunication` - where `sent_or_received`="Received" - and TIMEDIFF(%s, modified) < '01:00:00'""", - now(), - )[0][0] - > max_communications_per_hour - ): - frappe.response[ - "message" - ] = "Sorry: we believe we have received an unreasonably high number of requests of this kind. Please try later" - return - - # send email - forward_to_email = frappe.db.get_single_value("Contact Us Settings", "forward_to_email") - if forward_to_email: - frappe.sendmail(recipients=forward_to_email, sender=sender, content=message, subject=subject) + frappe.sendmail( + recipients=sender, + content="Thank you for reaching out to us. We will get back to you at the earliest.", + subject="We've received your query!", + ) # add to to-do ? frappe.get_doc( @@ -66,5 +45,3 @@ def send_message(subject="Website Query", message="", sender=""): status="Open", ) ).insert(ignore_permissions=True) - - return "okay" From 1cb5bfe8755da0a653c6846e407e0374c19ba1f9 Mon Sep 17 00:00:00 2001 From: phot0n Date: Mon, 3 Apr 2023 01:28:56 +0530 Subject: [PATCH 117/120] fix: remove methods arg from tate_limit decorator * feat(minor): send a copy of message to the sender/contactee * feat: validate sender email address for contact us page --- frappe/www/contact.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/www/contact.py b/frappe/www/contact.py index 15d639be97..b27685dfcb 100644 --- a/frappe/www/contact.py +++ b/frappe/www/contact.py @@ -4,6 +4,7 @@ import frappe from frappe import _ from frappe.rate_limiter import rate_limit +from frappe.utils import validate_email_address sitemap = 1 @@ -23,14 +24,15 @@ def get_context(context): @frappe.whitelist(allow_guest=True) -@rate_limit(limit=1000, seconds=60 * 60, methods=["POST"]) +@rate_limit(limit=1000, seconds=60 * 60) def send_message(sender, message, subject="Website Query"): + sender = validate_email_address(sender, throw=True) if forward_to_email := frappe.db.get_single_value("Contact Us Settings", "forward_to_email"): frappe.sendmail(recipients=forward_to_email, reply_to=sender, content=message, subject=subject) frappe.sendmail( recipients=sender, - content="Thank you for reaching out to us. We will get back to you at the earliest.", + content=f"
Thank you for reaching out to us. We will get back to you at the earliest.\n\n\nYour query:\n\n{message}
", subject="We've received your query!", ) From 0e4909b28b31c6bec85c48cda3972a973f3b7cb4 Mon Sep 17 00:00:00 2001 From: phot0n Date: Mon, 3 Apr 2023 02:31:03 +0530 Subject: [PATCH 118/120] refactor: remove frappe.send_message js util --- frappe/templates/includes/contact.js | 18 ++++++++++-------- frappe/website/js/website.js | 9 --------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/frappe/templates/includes/contact.js b/frappe/templates/includes/contact.js index 51dc5dc322..7074e54061 100644 --- a/frappe/templates/includes/contact.js +++ b/frappe/templates/includes/contact.js @@ -23,20 +23,22 @@ frappe.ready(function() { } $("#contact-alert").toggle(false); - frappe.send_message({ - subject: $('[name="subject"]').val(), - sender: email, - message: message, + frappe.call({ + type: "POST", + method: "frappe.www.contact.send_message", + args: { + subject: $('[name="subject"]').val(), + sender: email, + message: message, + }, callback: function(r) { if (!r.exc) { frappe.msgprint('{{ _("Thank you for your message") }}'); } $(':input').val(''); - } - }, this); - return false; + }, + }); }); - }); var msgprint = function(txt) { diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js index afe18d38ec..595234173f 100644 --- a/frappe/website/js/website.js +++ b/frappe/website/js/website.js @@ -213,15 +213,6 @@ $.extend(frappe, { ) .appendTo(document.body); }, - send_message: function (opts, btn) { - return frappe.call({ - type: "POST", - method: "frappe.www.contact.send_message", - btn: btn, - args: opts, - callback: opts.callback, - }); - }, has_permission: function (doctype, docname, perm_type, callback) { return frappe.call({ type: "GET", From cbe0ce37d3f344012db9f7502699ccd1786ad53d Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 4 Apr 2023 12:59:00 +0530 Subject: [PATCH 119/120] fix: suppress outgoing email error for contact-us page --- frappe/www/contact.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/frappe/www/contact.py b/frappe/www/contact.py index b27685dfcb..98aafdef49 100644 --- a/frappe/www/contact.py +++ b/frappe/www/contact.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +from contextlib import suppress + import frappe from frappe import _ from frappe.rate_limiter import rate_limit @@ -27,14 +29,19 @@ def get_context(context): @rate_limit(limit=1000, seconds=60 * 60) def send_message(sender, message, subject="Website Query"): sender = validate_email_address(sender, throw=True) - if forward_to_email := frappe.db.get_single_value("Contact Us Settings", "forward_to_email"): - frappe.sendmail(recipients=forward_to_email, reply_to=sender, content=message, subject=subject) - frappe.sendmail( - recipients=sender, - content=f"
Thank you for reaching out to us. We will get back to you at the earliest.\n\n\nYour query:\n\n{message}
", - subject="We've received your query!", - ) + with suppress(frappe.OutgoingEmailError): + if forward_to_email := frappe.db.get_single_value("Contact Us Settings", "forward_to_email"): + frappe.sendmail(recipients=forward_to_email, reply_to=sender, content=message, subject=subject) + + frappe.sendmail( + recipients=sender, + content=f"
Thank you for reaching out to us. We will get back to you at the earliest.\n\n\nYour query:\n\n{message}
", + subject="We've received your query!", + ) + + # for clearing outgoing email error message + frappe.clear_last_message() # add to to-do ? frappe.get_doc( From 41dc220e02862e276a6774d48c4eec5d572cb2b3 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 5 Apr 2023 00:42:15 +0000 Subject: [PATCH 120/120] ci: name step that installs and runs `pip-audit` (#20591) --- .github/workflows/linters.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index d050ecb6bc..be343c1254 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -92,7 +92,8 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - - run: | + - name: Install and run pip-audit + run: | pip install pip-audit cd ${GITHUB_WORKSPACE} sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456