feat(Integration): Google Contacts Integration (#7625)
This commit is contained in:
parent
8705ddfb0a
commit
dceb0d1b85
11 changed files with 703 additions and 877 deletions
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
0
frappe/integrations/doctype/google_contacts/__init__.py
Normal file
0
frappe/integrations/doctype/google_contacts/__init__.py
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
107
frappe/integrations/doctype/google_contacts/google_contacts.json
Normal file
107
frappe/integrations/doctype/google_contacts/google_contacts.json
Normal 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
|
||||
}
|
||||
166
frappe/integrations/doctype/google_contacts/google_contacts.py
Normal file
166
frappe/integrations/doctype/google_contacts/google_contacts.py
Normal 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 ""
|
||||
0
frappe/integrations/doctype/google_settings/__init__.py
Normal file
0
frappe/integrations/doctype/google_settings/__init__.py
Normal 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);
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue