diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 4df32c6705..1d3a5d644c 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -149,18 +149,26 @@ def get_permitted_and_not_permitted_links(doctype): return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links} -def delete_contact_and_address(doctype, docname): +def delete_contact_and_address(doctype: str, docname: str) -> None: for parenttype in ("Contact", "Address"): - items = frappe.db.sql_list( - """select parent from `tabDynamic Link` - where parenttype=%s and link_doctype=%s and link_name=%s""", - (parenttype, doctype, docname), - ) - - for name in items: + for name in frappe.get_all( + "Dynamic Link", + filters={ + "parenttype": parenttype, + "link_doctype": doctype, + "link_name": docname, + }, + pluck="parent", + ): doc = frappe.get_doc(parenttype, name) if len(doc.links) == 1: doc.delete() + else: + for link in doc.links: + if link.link_doctype == doctype and link.link_name == docname: + doc.remove(link) + doc.save() + break @frappe.whitelist() diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.js b/frappe/core/doctype/document_naming_rule/document_naming_rule.js index 70d95673e6..f6ea996107 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.js +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.js @@ -4,7 +4,24 @@ frappe.ui.form.on("Document Naming Rule", { refresh: function (frm) { frm.trigger("document_type"); - if (!frm.doc.__islocal) frm.trigger("add_update_counter_button"); + frm.last_counter_value = frm.doc.counter; + frm.skip_before_save = false; + }, + before_save: function (frm) { + if (frm.is_new() || frm.skip_before_save || frm.last_counter_value === frm.doc.counter) + return; + + frappe.validated = false; + frappe.warn( + __("Are you sure?"), + __("Updating counter may lead to document name conflicts if not done properly"), + () => { + frm.skip_before_save = true; + frm.save(); + }, + __("Proceed"), + false + ); }, document_type: (frm) => { // update the select field options with fieldnames @@ -26,45 +43,4 @@ frappe.ui.form.on("Document Naming Rule", { }); } }, - add_update_counter_button: (frm) => { - frm.add_custom_button(__("Update Counter"), function () { - const fields = [ - { - fieldtype: "Data", - fieldname: "new_counter", - label: __("New Counter"), - default: frm.doc.counter, - reqd: 1, - description: __( - "Warning: Updating counter may lead to document name conflicts if not done properly" - ), - }, - ]; - - let primary_action_label = __("Save"); - - let primary_action = (fields) => { - frappe.call({ - method: "frappe.core.doctype.document_naming_rule.document_naming_rule.update_current", - args: { - name: frm.doc.name, - new_counter: fields.new_counter, - }, - callback: function () { - frm.set_value("counter", fields.new_counter); - dialog.hide(); - }, - }); - }; - - const dialog = new frappe.ui.Dialog({ - title: __("Update Counter Value for Prefix: {0}", [frm.doc.prefix]), - fields, - primary_action_label, - primary_action, - }); - - dialog.show(); - }); - }, }); diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json index 4e6f3f3fd1..1e2247c250 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.json +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json @@ -12,8 +12,9 @@ "conditions", "naming_section", "prefix", - "prefix_digits", - "counter" + "counter", + "column_break_xfqa", + "prefix_digits" ], "fields": [ { @@ -22,7 +23,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Document Type", - "options": "DocType" + "options": "DocType", + "reqd": 1 }, { "default": "0", @@ -38,11 +40,12 @@ "reqd": 1 }, { + "default": "0", + "description": "Warning: Updating counter may lead to document name conflicts if not done properly", "fieldname": "counter", "fieldtype": "Int", "label": "Counter", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { "default": "5", @@ -76,11 +79,15 @@ "fieldname": "priority", "fieldtype": "Int", "label": "Priority" + }, + { + "fieldname": "column_break_xfqa", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-13 20:07:47.617615", + "modified": "2023-04-24 15:14:32.054272", "modified_by": "Administrator", "module": "Core", "name": "Document Naming Rule", @@ -102,6 +109,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "document_type", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 598de98dbb..46bc1c5ee2 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -47,9 +47,3 @@ class DocumentNamingRule(Document): doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1) frappe.db.set_value(self.doctype, self.name, "counter", counter + 1) - - -@frappe.whitelist() -def update_current(name, new_counter): - frappe.only_for("System Manager") - frappe.db.set_value("Document Naming Rule", name, "counter", new_counter) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index ba3846f8b5..4b6438fded 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -76,7 +76,9 @@ "max_auto_email_report_per_user", "system_updates_section", "disable_system_update_notification", - "disable_change_log_notification" + "disable_change_log_notification", + "telemetry_section", + "enable_telemetry" ], "fields": [ { @@ -535,12 +537,24 @@ "fieldname": "disable_document_sharing", "fieldtype": "Check", "label": "Disable Document Sharing" + }, + { + "collapsible": 1, + "fieldname": "telemetry_section", + "fieldtype": "Section Break", + "label": "Telemetry" + }, + { + "default": "1", + "fieldname": "enable_telemetry", + "fieldtype": "Check", + "label": "Allow Sending Usage Data for Improving applications" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-03-14 11:30:56.465653", + "modified": "2023-04-23 11:14:59.302851", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 918a9ee37c..e389a73cbb 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -114,9 +114,9 @@ frappe.ui.form.on("User", { return; } - function hasChanged(doc_attr, boot_attr) { - return (doc_attr || boot_attr) && doc_attr !== boot_attr; - } + const hasChanged = (doc_attr, boot_attr) => { + return doc_attr && boot_attr && doc_attr !== boot_attr; + }; if ( doc.name === frappe.session.user && diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index 2b17d38371..ce8bb444a1 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -147,35 +147,36 @@ def add_multiple(args=None): def close_all_assignments(doctype, name): assignments = frappe.get_all( "ToDo", - fields=["allocated_to"], + fields=["allocated_to", "name"], filters=dict(reference_type=doctype, reference_name=name, status=("!=", "Cancelled")), ) if not assignments: return False for assign_to in assignments: - set_status(doctype, name, assign_to.allocated_to, status="Closed") + set_status(doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Closed") return True @frappe.whitelist() def remove(doctype, name, assign_to): - return set_status(doctype, name, assign_to, status="Cancelled") + return set_status(doctype, name, "", assign_to, status="Cancelled") -def set_status(doctype, name, assign_to, status="Cancelled"): +def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"): """remove from todo""" try: - todo = frappe.db.get_value( - "ToDo", - { - "reference_type": doctype, - "reference_name": name, - "allocated_to": assign_to, - "status": ("!=", status), - }, - ) + if not todo: + todo = frappe.db.get_value( + "ToDo", + { + "reference_type": doctype, + "reference_name": name, + "allocated_to": assign_to, + "status": ("!=", status), + }, + ) if todo: todo = frappe.get_doc("ToDo", todo) todo.status = status @@ -197,13 +198,17 @@ def clear(doctype, name): Clears assignments, return False if not assigned. """ assignments = frappe.get_all( - "ToDo", fields=["allocated_to"], filters=dict(reference_type=doctype, reference_name=name) + "ToDo", + fields=["allocated_to", "name"], + filters=dict(reference_type=doctype, reference_name=name), ) if not assignments: return False for assign_to in assignments: - set_status(doctype, name, assign_to.allocated_to, "Cancelled") + set_status( + doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Cancelled" + ) return True diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 969aedb882..865619c9ff 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -178,6 +178,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } action_on_complete() { + frappe.telemetry.capture("initated_client_side", "setup"); if (!this.current_slide.set_values()) return; this.update_values(); this.show_working_state(); @@ -325,6 +326,7 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { make() { super.make(); this.set_init_values(); + this.setup_telemetry_events(); this.reset_action_button_state(); } @@ -340,6 +342,18 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { }); } } + + setup_telemetry_events() { + let me = this; + this.fields.filter(frappe.model.is_value_type).forEach((field) => { + me.get_input(field.fieldname).on("change", function () { + frappe.telemetry.capture(`${field.fieldname}_set`, "setup"); + if (field.fieldname == "enable_telemetry" && !me.get_value("enable_telemetry")) { + frappe.telemetry.disable(); + } + }); + }); + } }; // Frappe slides settings @@ -384,6 +398,15 @@ frappe.setup.slides_settings = [ fieldtype: "Select", reqd: 1, }, + { + fieldtype: "Section Break", + }, + { + fieldname: "enable_telemetry", + label: __("Allow Sending Usage Data for Improving applications"), + fieldtype: "Check", + default: 1, + }, ], onload: function (slide) { diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index cdb25b81ba..cb869fb5fc 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -64,6 +64,9 @@ def setup_complete(args): @frappe.task() def process_setup_stages(stages, user_input, is_background_task=False): + from frappe.utils.telemetry import capture + + capture("initated_server_side", "setup") try: frappe.flags.in_setup_wizard = True current_task = None @@ -88,6 +91,7 @@ def process_setup_stages(stages, user_input, is_background_task=False): ) else: run_setup_success(user_input) + capture("completed_server_side", "setup") if not is_background_task: return {"status": "ok"} frappe.publish_realtime("setup_task", {"status": "ok"}, user=frappe.session.user) @@ -171,6 +175,7 @@ def update_system_settings(args): "number_format": number_format, "enable_scheduler": 1 if not frappe.flags.in_test else 0, "backup_limit": 3, # Default for downloadable backups + "enable_telemetry": cint(args.get("enable_telemetry")), } ) system_settings.save() diff --git a/frappe/desk/search.py b/frappe/desk/search.py index ee63f67423..67695e4e73 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -81,7 +81,15 @@ def search_widget( try: is_whitelisted(frappe.get_attr(query)) frappe.response["values"] = frappe.call( - query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict + query, + doctype, + txt, + searchfield, + start, + page_length, + filters, + as_dict=as_dict, + reference_doctype=reference_doctype, ) except frappe.exceptions.PermissionError as e: if frappe.local.conf.developer_mode: diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index c10494b0d9..d254c87a0a 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -203,7 +203,7 @@ class SendMailContext: # Note: smtp session will have to be manually closed self.retain_smtp_session = bool(smtp_server_instance) - self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_main_sent()] + self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()] def __enter__(self): self.queue_doc.update_status(status="Sending", commit=True) @@ -213,7 +213,6 @@ class SendMailContext: exceptions = [ smtplib.SMTPServerDisconnected, smtplib.SMTPAuthenticationError, - smtplib.SMTPRecipientsRefused, smtplib.SMTPConnectError, smtplib.SMTPHeloError, JobTimeoutException, diff --git a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py index bcb8d9b05d..705075a862 100644 --- a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py +++ b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py @@ -11,7 +11,7 @@ class EmailQueueRecipient(Document): def is_mail_to_be_sent(self): return self.status == "Not Sent" - def is_main_sent(self): + def is_mail_sent(self): return self.status == "Sent" def update_db(self, commit=False, **kwargs): diff --git a/frappe/hooks.py b/frappe/hooks.py index a3355d5455..5967486824 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -29,6 +29,7 @@ app_include_js = [ "form.bundle.js", "controls.bundle.js", "report.bundle.js", + "telemetry.bundle.js", ] app_include_css = [ "desk.bundle.css", @@ -419,3 +420,7 @@ after_job = [ "frappe.monitor.stop", "frappe.utils.file_lock.release_document_locks", ] + +extend_bootinfo = [ + "frappe.utils.telemetry.add_bootinfo", +] diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 65a4a2bccd..d1cdd0d9e7 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -111,6 +111,7 @@ def authorize_access(g_calendar, reauthorize=None): """ google_settings = frappe.get_doc("Google Settings") google_calendar = frappe.get_doc("Google Calendar", g_calendar) + google_calendar.check_permission("write") redirect_uri = ( get_request_site_address(True) diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json index 76781fe47f..4a72651e67 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.json +++ b/frappe/integrations/doctype/google_contacts/google_contacts.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:GC-{email_id}", "creation": "2019-06-14 00:09:39.441961", "doctype": "DocType", @@ -97,10 +98,12 @@ "label": "Push to Google Contacts" } ], - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2023-03-30 11:25:48.832384", "modified_by": "Administrator", "module": "Integrations", "name": "Google Contacts", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -116,17 +119,14 @@ { "create": 1, "delete": 1, - "email": 1, - "export": 1, - "print": 1, + "if_owner": 1, "read": 1, - "report": 1, "role": "All", - "share": 1, "write": 1 } ], "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 9a20d5e905..7c845da330 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -36,10 +36,10 @@ def authorize_access(g_contact, reauthorize=False, code=None): If no Authorization code get it from Google and then request for Refresh Token. Google Contact Name is set to flags to set_value after Authorization Code is obtained. """ + contact = frappe.get_doc("Google Contacts", g_contact) + contact.check_permission("write") - oauth_code = ( - frappe.db.get_value("Google Contacts", g_contact, "authorization_code") if not code else code - ) + oauth_code = code or contact.get_password("authorization_code") oauth_obj = GoogleOAuth("contacts") if not oauth_code or reauthorize: @@ -51,11 +51,9 @@ def authorize_access(g_contact, reauthorize=False, code=None): ) r = oauth_obj.authorize(oauth_code) - frappe.db.set_value( - "Google Contacts", - g_contact, - {"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")}, - ) + contact.authorization_code = oauth_code + contact.refresh_token = r.get("refresh_token") + contact.save() def get_google_contacts_object(g_contact): diff --git a/frappe/integrations/doctype/google_contacts/test_google_contacts.py b/frappe/integrations/doctype/google_contacts/test_google_contacts.py new file mode 100644 index 0000000000..d7ca08a082 --- /dev/null +++ b/frappe/integrations/doctype/google_contacts/test_google_contacts.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestGoogleContacts(FrappeTestCase): + pass diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 6c6f617275..7e5eccf26d 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -348,11 +348,9 @@ $.extend(frappe.model, { }, unscrub: function (txt) { - return __(txt || "") - .replace(/-|_/g, " ") - .replace(/\w*/g, function (keywords) { - return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase(); - }); + return (txt || "").replace(/-|_/g, " ").replace(/\w*/g, function (keywords) { + return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase(); + }); }, can_create: function (doctype) { diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index efdba7d4d8..312a90a82c 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -59,7 +59,7 @@ frappe.RoleEditor = class { const $body = $(this.perm_dialog.body); if (!permissions.length) { $body.append(`