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/__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, diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 5fe22eb7f2..965425019c 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -254,19 +254,23 @@ 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 ({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 %(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 "", diff --git a/frappe/contacts/doctype/address/test_address.py b/frappe/contacts/doctype/address/test_address.py index 1d11c5efef..ecb95f9e0c 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) 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/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 7b6427d1c2..24b6a8fafb 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()) @@ -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 and self.reference_name: + return ToDo.get_owners(filters) + else: + return [] @staticmethod def filter_thread_notification_disbled_users(emails): 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 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/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() 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..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 Setup > Email > Email Account" - ), + _("Email Account not setup. Please create a new Email Account from Settings > Email Account"), raise_exception=frappe.OutgoingEmailError, ) 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("
  • field:[fieldname] - By Field
  • naming_series: - By Naming Series (field called naming_series must be present
  • Prompt - Prompt user for a name
  • [series] - Series by prefix (separated by a dot); for example PRE.#####
  • \n
  • 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, @@ -1720,11 +1724,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)", @@ -2323,6 +2327,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, 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, } 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): 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") 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",