Merge pull request #8359 from hrwX/google_cont

feat(Integrations): Google Contacts
This commit is contained in:
mergify[bot] 2019-09-13 11:16:09 +00:00 committed by GitHub
commit 0a46aa2fa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 269 additions and 77 deletions

View file

@ -61,6 +61,15 @@ frappe.ui.form.on("Contact", {
}
}
]);
},
sync_with_google_contacts: function(frm) {
if (frm.doc.sync_with_google_contacts) {
frappe.db.get_value("Google Contacts", {"email_id": frappe.session.user}, "name", (r) => {
if (r && r.name) {
frm.set_value("google_contacts", r.name);
}
})
}
}
});

View file

@ -14,14 +14,21 @@
"email_id",
"user",
"address",
"sync_with_google_contacts",
"cb00",
"status",
"salutation",
"designation",
"gender",
"phone",
"company_name",
"image",
"sb_00",
"google_contacts",
"google_contacts_id",
"cb_00",
"pulled_from_google_contacts",
"sb_01",
"email_ids",
"phone_nos",
"contact_details",
@ -29,10 +36,7 @@
"links",
"more_info",
"department",
"unsubscribed",
"column_break_17",
"source",
"google_contacts_description"
"unsubscribed"
],
"fields": [
{
@ -155,36 +159,23 @@
"fieldtype": "Data",
"label": "Designation"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "unsubscribed",
"fieldtype": "Check",
"label": "Unsubscribed"
},
{
"fieldname": "source",
"fieldtype": "Data",
"label": "Source"
},
{
"fieldname": "middle_name",
"fieldtype": "Data",
"label": "Middle Name"
},
{
"depends_on": "eval:doc.source==\"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",
@ -203,13 +194,52 @@
"fieldtype": "Table",
"label": "Phone Nos",
"options": "Contact Phone"
},
{
"default": "0",
"fieldname": "pulled_from_google_contacts",
"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
},
{
"fieldname": "company_name",
"fieldtype": "Data",
"label": "Company Name"
}
],
"icon": "fa fa-user",
"idx": 1,
"image_field": "image",
"modified": "2019-08-09 10:23:00.486673",
"modified_by": "Administrator",
"modified": "2019-09-13 15:50:38.999884",
"modified_by": "himanshu@erpnext.com",
"module": "Contacts",
"name": "Contact",
"name_case": "Title Case",

View file

@ -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):

View file

@ -136,6 +136,15 @@ doc_events = {
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
],
},
"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",
}
}

View file

@ -11,7 +11,12 @@
"cb_00",
"last_sync_on",
"authorization_code",
"refresh_token"
"refresh_token",
"next_sync_token",
"sync",
"pull_from_google_contacts",
"column_break_12",
"push_to_google_contacts"
],
"fields": [
{
@ -62,10 +67,38 @@
"fieldname": "authorize_google_contacts_access",
"fieldtype": "Button",
"label": "Authorize Google Contacts Access"
},
{
"fieldname": "next_sync_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Next Sync Token"
},
{
"depends_on": "enable",
"fieldname": "sync",
"fieldtype": "Section Break",
"label": "Sync"
},
{
"default": "0",
"fieldname": "pull_from_google_contacts",
"fieldtype": "Check",
"label": "Pull from Google Contacts"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "push_to_google_contacts",
"fieldtype": "Check",
"label": "Push to Google Contacts"
}
],
"modified": "2019-08-23 13:50:52.789503",
"modified_by": "Administrator",
"modified": "2019-09-13 15:53:19.569924",
"modified_by": "himanshu@erpnext.com",
"module": "Integrations",
"name": "Google Contacts",
"owner": "Administrator",

View file

@ -5,14 +5,16 @@
from __future__ import unicode_literals
import frappe
import requests
import googleapiclient.discovery
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):
@ -60,7 +62,7 @@ def authorize_access(g_contact, reauthorize=None):
if not google_contact.authorization_code or reauthorize:
frappe.cache().hset("google_contacts", "google_contact", google_contact.name)
return google_callback(client_id=google_settings.client_id, redirect_uri=redirect_uri)
return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri)
else:
try:
data = {
@ -83,21 +85,42 @@ def authorize_access(g_contact, reauthorize=None):
except Exception as e:
frappe.throw(e)
def get_authentication_url(client_id=None, redirect_uri=None):
return {
"url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri)
}
@frappe.whitelist()
def google_callback(client_id=None, redirect_uri=None, code=None):
def google_callback(code=None):
"""
Authorization code is sent to callback as per the API configuration
"""
if code is None:
return {
"url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri)
}
else:
google_contact = frappe.cache().hget("google_contacts", "google_contact")
frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code)
frappe.db.commit()
google_contact = frappe.cache().hget("google_contacts", "google_contact")
frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code)
frappe.db.commit()
authorize_access(google_contact)
authorize_access(google_contact)
def get_google_contacts_object(g_contact):
"""
Returns an object of Google Calendar along with Google Calendar doc.
"""
google_settings = frappe.get_doc("Google Settings")
account = frappe.get_doc("Google Contacts", g_contact)
credentials_dict = {
"token": account.get_access_token(),
"refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False),
"token_uri": get_auth_url(),
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
"scopes": "https://www.googleapis.com/auth/contacts"
}
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
google_contacts = googleapiclient.discovery.build("people", "v1", credentials=credentials)
return google_contacts, account
@frappe.whitelist()
def sync(g_contact=None):
@ -108,58 +131,143 @@ def sync(g_contact=None):
google_contacts = frappe.get_list("Google Contacts", filters=filters)
for google_contact in google_contacts:
doc = frappe.get_doc("Google Contacts", google_contact.name)
access_token = doc.get_access_token()
for g in google_contacts:
return sync_contacts_from_google_contacts(g.name)
headers = {"Authorization": "Bearer {}".format(access_token)}
def sync_contacts_from_google_contacts(g_contact):
"""
Syncs Contacts from Google Contacts.
https://developers.google.com/people/api/rest/v1/people.connections/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:
r = requests.get(REQUEST, headers=headers, params=PARAMS)
except Exception as e:
frappe.throw(e)
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
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 {0}, error code {1}.").format(account.name, err.resp.status))
try:
r = r.json()
except Exception as e:
# if request doesn't return json show HTML ask permissions or to identify the error on google side
frappe.throw(e)
for contact in contacts.get("connections"):
results.append(contact)
connections = r.get("connections")
contacts_updated = 0
if not contacts.get("nextPageToken"):
if contacts.get("nextSyncToken"):
frappe.db.set_value("Google Contacts", account.name, "next_sync_token", contacts.get("nextSyncToken"))
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())
if connections:
for idx, connection in enumerate(connections):
frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=r.get("totalPeople")), user=frappe.session.user)
for idx, connection in enumerate(results):
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"):
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",
"google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name")
})
for name in connection.get("names"):
if name.get("metadata").get("primary"):
contacts_updated += 1
contact = frappe.get_doc({
"doctype": "Contact",
"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"),
"pulled_from_google_contacts": 1,
"google_contacts": account.name,
"company_name": 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)
for email in connection.get("emailAddresses", []):
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)
for phone in connection.get("phoneNumbers", []):
contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("metadata").get("primary") else 0)
contact.insert(ignore_permissions=True)
contact.insert(ignore_permissions=True)
if g_contact:
return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 else _("No new Google Contacts synced.")
return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 \
else _("No new Google Contacts synced.")
if g_contact:
return _("No Google Contacts present to sync.") # If no Google Contacts to sync
def insert_contacts_to_google_contacts(doc, method=None):
"""
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.is_new() \
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,"etag":contact.get("etag")},
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: