From b53203e14c7cc57ba2914d99b0257931c75b2761 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 16 Jun 2024 17:49:16 +0200 Subject: [PATCH 1/4] feat: download Contact as vCard --- frappe/contacts/doctype/contact/contact.js | 11 +++++ frappe/contacts/doctype/contact/contact.py | 50 ++++++++++++++++++++++ pyproject.toml | 1 + 3 files changed, 62 insertions(+) diff --git a/frappe/contacts/doctype/contact/contact.js b/frappe/contacts/doctype/contact/contact.js index f1a40e106f..71571c79e5 100644 --- a/frappe/contacts/doctype/contact/contact.js +++ b/frappe/contacts/doctype/contact/contact.js @@ -88,6 +88,17 @@ frappe.ui.form.on("Contact", { ); } } + + if (!frm.is_dirty()) { + frm.page.add_menu_item(__("Download vCard"), function () { + window.open( + `/api/method/frappe.contacts.doctype.contact.contact.download_vcard?contact=${encodeURIComponent( + frm.doc.name + )}`, + "_blank" + ); + }); + } }, validate: function (frm) { // clear linked customer / supplier / sales partner on saving... diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 2aa497b6b4..7564de029d 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -167,6 +167,56 @@ class Contact(Document): def _get_full_name(self) -> str: return get_full_name(self.first_name, self.middle_name, self.last_name, self.company_name) + def get_vcard(self): + from vobject import vCard + from vobject.vcard import Name + + vcard = vCard() + vcard.add("fn").value = self.full_name + + name = Name() + if self.first_name: + name.given = self.first_name + + if self.last_name: + name.family = self.last_name + + if self.middle_name: + name.additional = self.middle_name + + vcard.add("n").value = name + + if self.designation: + vcard.add("title").value = self.designation + + for row in self.email_ids: + email = vcard.add("email") + email.value = row.email_id + if row.is_primary: + email.type_param = "pref" + + for row in self.phone_nos: + tel = vcard.add("tel") + tel.value = row.phone + if row.is_primary_phone: + tel.type_param = "home" + + if row.is_primary_mobile_no: + tel.type_param = "cell" + + return vcard + + +@frappe.whitelist() +def download_vcard(contact: str): + """Download vCard for the contact""" + contact = frappe.get_doc("Contact", contact) + vcard = contact.get_vcard() + + frappe.response["filename"] = f"{contact.name}.vcf" + frappe.response["filecontent"] = vcard.serialize().encode("utf-8") + frappe.response["type"] = "binary" + def get_default_contact(doctype, name): """Return default contact for the given doctype, name.""" diff --git a/pyproject.toml b/pyproject.toml index 67d98546e1..79c4d519b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ dependencies = [ "google-auth-oauthlib~=0.4.4", "google-auth~=1.29.0", "posthog~=3.0.1", + "vobject~=0.9.7", ] [project.urls] From 9d19701e567f92a86e579b5b60c1ee82cb12f87a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 16 Jun 2024 18:19:48 +0200 Subject: [PATCH 2/4] feat: download contact list as vCards --- frappe/contacts/doctype/contact/contact.py | 18 ++++++++++++++++++ .../contacts/doctype/contact/contact_list.js | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 7564de029d..8aba7e0566 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -218,6 +218,24 @@ def download_vcard(contact: str): frappe.response["type"] = "binary" +@frappe.whitelist() +def download_vcards(contacts: str): + """Download vCard for the contact""" + from frappe.utils.data import now + + vcards = [] + for contact_id in frappe.parse_json(contacts): + contact = frappe.get_doc("Contact", contact_id) + vcard = contact.get_vcard() + vcards.append(vcard.serialize()) + + timestamp = now()[:19] # remove milliseconds + + frappe.response["filename"] = f"{timestamp} Contacts.vcf" + frappe.response["filecontent"] = "\n".join(vcards).encode("utf-8") + frappe.response["type"] = "binary" + + def get_default_contact(doctype, name): """Return default contact for the given doctype, name.""" out = frappe.db.sql( diff --git a/frappe/contacts/doctype/contact/contact_list.js b/frappe/contacts/doctype/contact/contact_list.js index 2b3cd8a062..07b4aeb570 100644 --- a/frappe/contacts/doctype/contact/contact_list.js +++ b/frappe/contacts/doctype/contact/contact_list.js @@ -1,3 +1,11 @@ frappe.listview_settings["Contact"] = { add_fields: ["image"], + onload: function (listview) { + listview.page.add_action_item(__("Download vCards"), function () { + const contacts = listview.get_checked_items(); + open_url_post("/api/method/frappe.contacts.doctype.contact.contact.download_vcards", { + contacts: contacts.map((c) => c.name), + }); + }); + }, }; From e61f1f99d6e4c82f0ac86eef68ae5c15abae4723 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:47:51 +0200 Subject: [PATCH 3/4] fix: check permission and create access log --- frappe/contacts/doctype/contact/contact.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 8aba7e0566..f638d70d4a 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -3,6 +3,7 @@ import frappe from frappe import _ from frappe.contacts.address_and_contact import set_link_title +from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links from frappe.model.document import Document from frappe.model.naming import append_number_if_name_exists @@ -211,7 +212,10 @@ class Contact(Document): def download_vcard(contact: str): """Download vCard for the contact""" contact = frappe.get_doc("Contact", contact) + contact.check_permission() + vcard = contact.get_vcard() + make_access_log(doctype="Contact", document=contact.name, file_type="vcf") frappe.response["filename"] = f"{contact.name}.vcf" frappe.response["filecontent"] = vcard.serialize().encode("utf-8") @@ -223,12 +227,19 @@ def download_vcards(contacts: str): """Download vCard for the contact""" from frappe.utils.data import now + contact_ids = frappe.parse_json(contacts) + vcards = [] - for contact_id in frappe.parse_json(contacts): + for contact_id in contact_ids: contact = frappe.get_doc("Contact", contact_id) + contact.check_permission() vcard = contact.get_vcard() vcards.append(vcard.serialize()) + # Separate loop in order to avoid access logs for errored exports + for contact_id in contact_ids: + make_access_log(doctype="Contact", document=contact_id, file_type="vcf") + timestamp = now()[:19] # remove milliseconds frappe.response["filename"] = f"{timestamp} Contacts.vcf" From be29009e4a83515333b716487031e4df117d54cc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:03:25 +0200 Subject: [PATCH 4/4] refactor: only one access log for bulk contact export --- frappe/contacts/doctype/contact/contact.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index f638d70d4a..8cbe9898ed 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -225,6 +225,8 @@ def download_vcard(contact: str): @frappe.whitelist() def download_vcards(contacts: str): """Download vCard for the contact""" + import json + from frappe.utils.data import now contact_ids = frappe.parse_json(contacts) @@ -236,9 +238,11 @@ def download_vcards(contacts: str): vcard = contact.get_vcard() vcards.append(vcard.serialize()) - # Separate loop in order to avoid access logs for errored exports - for contact_id in contact_ids: - make_access_log(doctype="Contact", document=contact_id, file_type="vcf") + make_access_log( + doctype="Contact", + filters=json.dumps([["name", "in", contact_ids]], ensure_ascii=False, indent="\t"), + file_type="vcf", + ) timestamp = now()[:19] # remove milliseconds