From 6de3e9a1bf5232b0f278213abfbde0f5ec6ca076 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 13:56:00 +0530 Subject: [PATCH] feat: push contacts to google contacts --- frappe/contacts/doctype/contact/contact.json | 52 +++++++-- frappe/contacts/doctype/contact/contact.py | 3 + frappe/hooks.py | 12 ++ .../google_contacts/google_contacts.py | 103 +++++++++++++++--- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index fe692a0ff5..45f843428f 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -1,4 +1,5 @@ { + "_comments": "[]", "allow_events_in_timeline": 1, "allow_import": 1, "allow_rename": 1, @@ -21,7 +22,14 @@ "gender", "phone", "image", + "sync_with_google_contacts", "sb_00", + "google_contacts", + "google_contacts_id", + "cb_00", + "pulled_from_google_contacts", + "google_contacts_description", + "sb_01", "email_ids", "phone_nos", "contact_details", @@ -29,10 +37,7 @@ "links", "more_info", "department", - "unsubscribed", - "column_break_17", - "pulled_from_google_contacts", - "google_contacts_description" + "unsubscribed" ], "fields": [ { @@ -155,10 +160,6 @@ "fieldtype": "Data", "label": "Designation" }, - { - "fieldname": "column_break_17", - "fieldtype": "Column Break" - }, { "default": "0", "fieldname": "unsubscribed", @@ -171,15 +172,17 @@ "label": "Middle Name" }, { - "depends_on": "eval:doc.source==\"Google Contacts\"", + "depends_on": "pulled_from_google_contacts", "fieldname": "google_contacts_description", "fieldtype": "Small Text", "label": "Google Contacts Description" }, { + "collapsible": 1, + "depends_on": "eval:doc.sync_with_google_contacts || doc.pulled_from_google_contacts", "fieldname": "sb_00", "fieldtype": "Section Break", - "label": "Contact Details" + "label": "Google Contacts" }, { "fieldname": "email_ids", @@ -205,12 +208,39 @@ "fieldtype": "Check", "label": "Pulled from Google Contacts", "read_only": 1 + }, + { + "default": "0", + "fieldname": "sync_with_google_contacts", + "fieldtype": "Check", + "label": "Sync with Google Contacts" + }, + { + "fieldname": "google_contacts", + "fieldtype": "Link", + "label": "Google Contacts", + "options": "Google Contacts" + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "sb_01", + "fieldtype": "Section Break", + "label": "Contact Details" + }, + { + "fieldname": "google_contacts_id", + "fieldtype": "Data", + "label": "Google Contacts Id", + "read_only": 1 } ], "icon": "fa fa-user", "idx": 1, "image_field": "image", - "modified": "2019-09-06 06:43:02.159233", + "modified": "2019-09-06 20:23:15.088231", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 085452988a..6857d013a1 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -42,6 +42,9 @@ class Contact(Document): if self.email_id and not self.image: self.image = has_gravatar(self.email_id) + if self.sync_with_google_contacts and not self.google_contacts: + frappe.throw(_("Select Google Contacts to which contact should be synced.")) + deduplicate_dynamic_links(self) def set_user(self): diff --git a/frappe/hooks.py b/frappe/hooks.py index eb0d5bbe77..8243283353 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -136,6 +136,18 @@ doc_events = { "on_change": [ "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" ], + }, + "Email Group Member": { + "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" + }, + "Event": { + "after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar", + "on_update": "frappe.integrations.doctype.google_calendar.google_calendar.update_event_in_google_calendar", + "on_trash": "frappe.integrations.doctype.google_calendar.google_calendar.delete_event_from_google_calendar", + }, + "Contact": { + "after_insert": "frappe.integrations.doctype.google_contacts.google_contacts.insert_contacts_to_google_contacts", + "on_update": "frappe.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts", } } diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 609ee19728..aaae39b264 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -10,12 +10,11 @@ import google.oauth2.credentials from frappe.model.document import Document from frappe import _ +from googleapiclient.errors import HttpError from frappe.utils import get_request_site_address from frappe.integrations.doctype.google_settings.google_settings import get_auth_url SCOPES = "https://www.googleapis.com/auth/contacts" -REQUEST = "https://people.googleapis.com/v1/people/me/connections" -PARAMS = {"personFields": "names,emailAddresses,organizations,phoneNumbers"} class GoogleContacts(Document): @@ -135,24 +134,28 @@ def sync(g_contact=None): for g in google_contacts: return sync_contacts_from_google_contacts(g.name) -def sync_contacts_from_google_contacts(g_contact, page_length=10): +def sync_contacts_from_google_contacts(g_contact): """ Syncs Contacts from Google Contacts. https://developers.google.com/people/api/rest/v1/contactGroups/list """ google_contacts, account = get_google_contacts_object(g_contact) + + if not account.pull_from_google_contacts: + return + results = [] contacts_updated = 0 while True: try: sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None - contacts = google_contacts.people().connections().list(resourceName='people/me', maxResults=page_length, - syncToken=sync_token).execute() + contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token, + personFields="names,emailAddresses,organizations,phoneNumbers").execute() except HttpError as err: - frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts, error code {0}.").format(err.resp.status)) + frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) - for contact in contacts.get("items"): + for contact in contacts.get("connections"): results.append(contact) if not contacts.get("nextPageToken"): @@ -161,29 +164,29 @@ def sync_contacts_from_google_contacts(g_contact, page_length=10): frappe.db.commit() break - frappe.db.set_value("Google Contacts", doc.name, "last_sync_on", frappe.utils.now_datetime()) + frappe.db.set_value("Google Contacts", account.name, "last_sync_on", frappe.utils.now_datetime()) for idx, connection in enumerate(results): - frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=r.get("totalPeople")), user=frappe.session.user) + frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=len(results)), user=frappe.session.user) for name in connection.get("names"): if name.get("metadata").get("primary"): + contacts_updated += 1 contact = frappe.get_doc({ "doctype": "Contact", - "salutation": name.get("honorificPrefix") or "", "first_name": name.get("givenName") or "", "middle_name": name.get("middleName") or "", "last_name": name.get("familyName") or "", "designation": get_indexed_value(connection.get("organizations"), 0, "title"), - "source": "Google Contacts", + "pulled_from_google_contacts": 1, "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") }) for email in connection.get("emailAddresses", []): - contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("primary") else 0) + contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("metadata").get("primary") else 0) for phone in connection.get("phoneNumbers", []): - contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("primary") else 0) + contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("metadata").get("primary") else 0) contact.insert(ignore_permissions=True) @@ -191,7 +194,79 @@ def sync_contacts_from_google_contacts(g_contact, page_length=10): else _("No new Google Contacts synced.") def insert_contacts_to_google_contacts(doc, method=None): - pass + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people/createContact + """ + if not frappe.db.exists("Google Contacts", {"name": doc.google_contacts}) or doc.pulled_from_google_contacts \ + or not doc.sync_with_google_contacts: + return + + google_contacts, account = get_google_contacts_object(doc.google_contacts) + + if not account.push_to_google_contacts: + return + + names = { + "givenName": doc.first_name, + "middleName": doc.middle_name, + "familyName": doc.last_name + } + + phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos] + emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids] + + try: + contact = google_contacts.people().createContact(parent='people/me', body={"names": [names],"phoneNumbers": phoneNumbers, + "emailAddresses": emailAddresses}).execute() + frappe.db.set_value("Contact", doc.name, "google_contacts_id", contact.get("resourceName")) + except HttpError as err: + frappe.msgprint(_("Google Calendar - Could not insert contact in Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) + +def update_contacts_to_google_contacts(doc, method=None): + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people/updateContact + """ + # Workaround to avoid triggering updation when Event is being inserted since + # creation and modified are same when inserting doc + if not frappe.db.exists("Google Contacts", {"name": doc.google_contacts}) or doc.modified == doc.creation \ + or not doc.sync_with_google_contacts: + return + + if doc.sync_with_google_contacts and not doc.google_contacts_id: + # If sync_with_google_contacts is checked later, then insert the contact rather than updating it. + insert_contacts_to_google_contacts(doc) + return + + google_contacts, account = get_google_contacts_object(doc.google_contacts) + + if not account.push_to_google_contacts: + return + + names = { + "givenName": doc.first_name, + "middleName": doc.middle_name, + "familyName": doc.last_name + } + + phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos] + emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids] + + try: + contact = google_contacts.people().get(resourceName=doc.google_contacts_id, \ + personFields="names,emailAddresses,organizations,phoneNumbers").execute() + + contact["names"] = [names] + contact["phoneNumbers"] = phoneNumbers + contact["emailAddresses"] = emailAddresses + + google_contacts.people().updateContact(resourceName=doc.google_contacts_id,body={"names":[names], + "phoneNumbers":phoneNumbers,"emailAddresses":emailAddresses}, + updatePersonFields="names,emailAddresses,organizations,phoneNumbers").execute() + frappe.msgprint(_("Contact Synced with Google Contacts.")) + except HttpError as err: + frappe.msgprint(_("Google Contacts - Could not update contact in Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) def get_indexed_value(d, index, key): if not d: