feat(Integration): Google Contacts Integration (#7625)

This commit is contained in:
Himanshu 2019-06-20 11:26:07 +05:30 committed by Faris Ansari
parent 8705ddfb0a
commit dceb0d1b85
11 changed files with 703 additions and 877 deletions

View file

@ -87,6 +87,11 @@ def get_data():
{
"label": _("Google Services"),
"items": [
{
"type": "doctype",
"name": "Google Settings",
"description": _("Google API Settings."),
},
{
"type": "doctype",
"name": "Google Maps Settings",
@ -111,6 +116,11 @@ def get_data():
"type": "doctype",
"name": "GSuite Templates",
"description": _("Google GSuite Templates to integration with DocTypes"),
},
{
"type": "doctype",
"name": "Google Contacts",
"description": _("Google Contacts Integration."),
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -160,7 +160,7 @@ scheduler_events = {
"frappe.limits.update_site_usage",
"frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates"
"frappe.desk.form.document_follow.send_hourly_updates",
],
"daily": [
"frappe.email.queue.clear_outbox",
@ -176,7 +176,8 @@ scheduler_events = {
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
"frappe.desk.form.document_follow.send_daily_updates",
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points"
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
"frappe.integrations.doctype.google_contacts.google_contacts.sync",
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",

View file

@ -0,0 +1,56 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Google Contacts', {
refresh: function(frm) {
frm.set_value("user", frappe.session.user);
frappe.realtime.on('import_google_contacts', (data) => {
if (data.progress) {
frm.dashboard.show_progress('Import Google Contacts', data.progress / data.total * 100,
__('Importing {0} of {1}', [data.progress, data.total]));
if (data.progress === data.total) {
frm.dashboard.hide_progress('Import Google Contacts');
}
}
});
if (frm.doc.refresh_token) {
frm.add_custom_button(__('Sync Contacts'), function () {
frappe.show_alert({
indicator: 'green',
message: __('Syncing')
});
frappe.call({
method: "frappe.integrations.doctype.google_contacts.google_contacts.sync",
args: {
"g_contact": frm.doc.name
},
}).then((r) => {
frappe.hide_progress();
frappe.msgprint(r.message);
});
});
}
},
authorize_google_contacts_access: function(frm) {
let reauthorize = 0;
if(frm.doc.authorization_code) {
reauthorize = 1;
}
frappe.call({
method: "frappe.integrations.doctype.google_contacts.google_contacts.authorize_access",
args: {
"g_contact": frm.doc.name,
"reauthorize": reauthorize
},
callback: function(r) {
if(!r.exc) {
frm.save();
window.open(r.message.url);
}
}
});
}
});

View file

@ -0,0 +1,107 @@
{
"autoname": "format:GC-{user}",
"creation": "2019-06-14 00:09:39.441961",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enable",
"sb_00",
"email_id",
"authorize_google_contacts_access",
"cb_00",
"user",
"last_sync_on",
"authorization_code",
"refresh_token"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"options": "User",
"read_only": 1
},
{
"fieldname": "authorization_code",
"fieldtype": "Password",
"hidden": 1,
"label": "Authorization Code"
},
{
"fieldname": "refresh_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Refresh Token"
},
{
"fieldname": "last_sync_on",
"fieldtype": "Datetime",
"label": "Last Sync On",
"read_only": 1
},
{
"description": "Email Address whose Google Contacts are to be synced.",
"fieldname": "email_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Email Address",
"options": "Email",
"reqd": 1
},
{
"depends_on": "enable",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "Google Contacts"
},
{
"fieldname": "cb_00",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "authorize_google_contacts_access",
"fieldtype": "Button",
"label": "Authorize Google Contacts Access"
}
],
"modified": "2019-06-19 15:26:24.494101",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Contacts",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View file

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import requests
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_request_site_address
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):
def validate(self):
if not frappe.db.get_single_value("Google Settings", "enable"):
frappe.throw(_("Enable Google API in Google Settings."))
def get_access_token(self):
google_settings = frappe.get_doc("Google Settings")
if not google_settings.enable:
frappe.throw(_("Google Contacts Integration is disabled."))
if not self.refresh_token:
button_label = frappe.bold(_('Allow Google Contacts Access'))
raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label))
data = {
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
"refresh_token": self.get_password(fieldname="refresh_token", raise_exception=False),
"grant_type": "refresh_token",
"scope": SCOPES
}
try:
r = requests.post("https://www.googleapis.com/oauth2/v4/token", data=data).json()
except requests.exceptions.HTTPError:
button_label = frappe.bold(_('Allow Google Contacts Access'))
frappe.throw(_("Something went wrong during the token generation. Click on {0} to generate a new one.").format(button_label))
return r.get("access_token")
@frappe.whitelist()
def authorize_access(g_contact, reauthorize=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.
"""
google_settings = frappe.get_doc("Google Settings")
google_contact = frappe.get_doc("Google Contacts", g_contact)
redirect_uri = get_request_site_address(True) + "?cmd=frappe.integrations.doctype.google_contacts.google_contacts.google_callback"
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)
else:
try:
data = {
"code": google_contact.authorization_code,
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
"redirect_uri": redirect_uri,
"grant_type": "authorization_code"
}
r = requests.post("https://www.googleapis.com/oauth2/v4/token", data=data).json()
if "refresh_token" in r:
frappe.db.set_value("Google Contacts", google_contact.name, "refresh_token", r.get("refresh_token"))
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/desk#Form/Google%20Contacts/{}".format(google_contact.name)
frappe.msgprint(_("Google Contacts has been configured."))
except Exception as e:
frappe.throw(e)
@frappe.whitelist()
def google_callback(client_id=None, redirect_uri=None, 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)
authorize_access(google_contact)
@frappe.whitelist()
def sync(g_contact=None):
filters = {"enable": 1}
if g_contact:
filters.update({"name": g_contact})
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()
headers = {"Authorization": "Bearer {}".format(access_token)}
try:
r = requests.get(REQUEST, headers=headers, params=PARAMS)
except Exception as e:
frappe.throw(e)
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)
connections = r.get("connections")
contacts_updated = 0
frappe.db.set_value("Google Contacts", doc.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 name in connection.get("names"):
if name.get("metadata").get("primary"):
for email in connection.get("emailAddresses"):
if not frappe.db.exists("Contact", {"email_id": email.get("value")}):
contacts_updated += 1
frappe.get_doc({
"doctype": "Contact",
"salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "",
"first_name": name.get("givenName") if name.get("givenName") else "",
"middle_name": name.get("middleName") if name.get("middleName") else "",
"last_name": name.get("familyName") if name.get("familyName") else "",
"email_id": email.get("value") if email.get("value") else "",
"designation": get_indexed_value(connection.get("organizations"), 0, "title"),
"phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"),
"mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"),
"source": "Google Contacts",
"google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name")
}).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.")
if g_contact:
return _("No Google Contacts present to sync.") # If no Google Contacts to sync
def get_indexed_value(d, index, key):
if not d:
return ""
try:
return d[index].get(key)
except IndexError:
return ""

View file

@ -0,0 +1,9 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Google Settings', {
enable: function(frm) {
frm.set_df_property('client_id', 'reqd', frm.doc.enable ? 1 : 0);
frm.set_df_property('client_secret', 'reqd', frm.doc.enable ? 1 : 0);
}
});

View file

@ -0,0 +1,69 @@
{
"creation": "2019-06-14 00:08:37.255003",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enable",
"google_credentials",
"client_id",
"client_secret"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "google_credentials",
"fieldtype": "Section Break",
"label": "Google Credentials"
},
{
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID"
},
{
"fieldname": "client_secret",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Client Secret"
}
],
"issingle": 1,
"modified": "2019-06-19 15:28:05.957380",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class GoogleSettings(Document):
pass