[New Feature] Google Calendar Connector (#5266)

* Addition of a filter for last sync timestamp

* Google calendar connector wip

* Google calendar integration

* Add test for account creation

* Codacy corrections

* Remove unused import

* New section Google Services

* Add no_copy to migration custom field
This commit is contained in:
Charles-Henri Decultot 2018-04-06 07:13:49 +02:00 committed by Rushabh Mehta
parent 5307149a48
commit 68720c2a4f
26 changed files with 1695 additions and 19 deletions

View file

@ -70,8 +70,33 @@ def get_data():
]
},
{
"label": _("External Documents"),
"label": _("Webhook"),
"items": [
{
"type": "doctype",
"name": "Webhook",
"description": _("Webhooks calling API requests into web apps"),
}
]
},
{
"label": _("Google Services"),
"items": [
{
"type": "doctype",
"name": "Google Maps",
"description": _("Google Maps integration"),
},
{
"type": "doctype",
"name": "GCalendar Settings",
"description": _("Configure your google calendar integration"),
},
{
"type": "doctype",
"name": "GCalendar Account",
"description": _("Configure accounts for google calendar"),
},
{
"type": "doctype",
"name": "GSuite Settings",
@ -81,21 +106,6 @@ def get_data():
"type": "doctype",
"name": "GSuite Templates",
"description": _("Google GSuite Templates to integration with DocTypes"),
},
{
"type": "doctype",
"name": "Webhook",
"description": _("Webhooks calling API requests into web apps"),
}
]
},
{
"label": _("Maps"),
"items": [
{
"type": "doctype",
"name": "Google Maps",
"description": _("Google Maps integration"),
}
]
}

View file

@ -0,0 +1,239 @@
from __future__ import unicode_literals
import frappe
from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
import googleapiclient.discovery
import google.oauth2.credentials
from googleapiclient.errors import HttpError
import time
from datetime import datetime
from frappe.utils import add_days
class CalendarConnector(BaseConnection):
def __init__(self, connector):
self.connector = connector
settings = frappe.get_doc("GCalendar Settings", None)
self.account = frappe.get_doc("GCalendar Account", connector.username)
self.credentials_dict = {
'token': self.account.get_password(fieldname='session_token', raise_exception=False),
'refresh_token': self.account.get_password(fieldname='refresh_token', raise_exception=False),
'token_uri': 'https://www.googleapis.com/oauth2/v4/token',
'client_id': settings.client_id,
'client_secret': settings.get_password(fieldname='client_secret', raise_exception=False),
'scopes':'https://www.googleapis.com/auth/calendar'
}
self.name_field = 'id'
self.credentials = google.oauth2.credentials.Credentials(**self.credentials_dict)
self.gcalendar = googleapiclient.discovery.build('calendar', 'v3', credentials=self.credentials)
self.check_remote_calendar()
def check_remote_calendar(self):
def _create_calendar():
timezone = frappe.db.get_value("System Settings", None, "time_zone")
calendar = {
'summary': self.account.calendar_name,
'timeZone': timezone
}
try:
created_calendar = self.gcalendar.calendars().insert(body=calendar).execute()
frappe.db.set_value("GCalendar Account", self.account.name, "gcalendar_id", created_calendar["id"])
except Exception:
frappe.log_error(frappe.get_traceback())
try:
if self.account.gcalendar_id is not None:
try:
self.gcalendar.calendars().get(calendarId=self.account.gcalendar_id).execute()
except Exception:
frappe.log_error(frappe.get_traceback())
else:
_create_calendar()
except HttpError as err:
if err.resp.status in [403, 500, 503]:
time.sleep(5)
elif err.resp.status in [404]:
_create_calendar()
else: raise
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
return self.get_events(remote_objectname, filters, page_length)
def insert(self, doctype, doc):
if doctype == 'Events':
from frappe.desk.doctype.event.event import has_permission
d = frappe.get_doc("Event", doc["name"])
if has_permission(d, self.account.name):
if doc["start_datetime"] >= datetime.now():
try:
doctype = "Event"
e = self.insert_events(doctype, doc)
return e
except Exception:
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
def update(self, doctype, doc, migration_id):
if doctype == 'Events':
from frappe.desk.doctype.event.event import has_permission
d = frappe.get_doc("Event", doc["name"])
if has_permission(d, self.account.name):
if doc["start_datetime"] >= datetime.now() and migration_id is not None:
try:
doctype = "Event"
return self.update_events(doctype, doc, migration_id)
except Exception:
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
def delete(self, doctype, migration_id):
if doctype == 'Events':
try:
return self.delete_events(migration_id)
except Exception:
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
def get_events(self, remote_objectname, filters, page_length):
page_token = None
results = []
while True:
events = self.gcalendar.events().list(calendarId=self.account.gcalendar_id, maxResults=page_length, singleEvents=False, showDeleted=True).execute()
for event in events['items']:
results.append(event)
page_token = events.get('nextPageToken')
if not page_token:
break
return list(results)
def insert_events(self, doctype, doc, migration_id=None):
event = {
'summary': doc.summary,
'description': doc.description
}
dates = self.return_dates(doc)
event.update(dates)
if migration_id:
event.update({"id": migration_id})
if doc.repeat_this_event != 0:
recurrence = self.return_recurrence(doctype, doc)
if not not recurrence:
event.update({"recurrence": ["RRULE:" + str(recurrence)]})
try:
remote_event = self.gcalendar.events().insert(calendarId=self.account.gcalendar_id, body=event).execute()
return {self.name_field: remote_event["id"]}
except Exception:
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
def update_events(self, doctype, doc, migration_id):
try:
event = self.gcalendar.events().get(calendarId=self.account.gcalendar_id, eventId=migration_id).execute()
event = {
'summary': doc.summary,
'description': doc.description
}
if doc.event_type == "Cancel":
event.update({"status": "cancelled"})
dates = self.return_dates(doc)
event.update(dates)
if doc.repeat_this_event != 0:
recurrence = self.return_recurrence(doctype, doc)
if recurrence:
event.update({"recurrence": ["RRULE:" + str(recurrence)]})
try:
updated_event = self.gcalendar.events().update(calendarId=self.account.gcalendar_id, eventId=migration_id, body=event).execute()
return {self.name_field: updated_event["id"]}
except Exception as e:
frappe.log_error(e, "GCalendar Synchronization Error")
except HttpError as err:
if err.resp.status in [404]:
self.insert_events(doctype, doc, migration_id)
else:
frappe.log_error(err.resp, "GCalendar Synchronization Error")
def delete_events(self, migration_id):
try:
self.gcalendar.events().delete(calendarId=self.account.gcalendar_id, eventId=migration_id).execute()
except HttpError as err:
if err.resp.status in [410]:
pass
def return_dates(self, doc):
timezone = frappe.db.get_value("System Settings", None, "time_zone")
if doc.end_datetime is None:
doc.end_datetime = doc.start_datetime
if doc.all_day == 1:
return {
'start': {
'date': doc.start_datetime.date().isoformat(),
'timeZone': timezone,
},
'end': {
'date': doc.start_datetime.date().isoformat(),
'timeZone': timezone,
}
}
else:
return {
'start': {
'dateTime': doc.start_datetime.isoformat(),
'timeZone': timezone,
},
'end': {
'dateTime': doc.end_datetime.isoformat(),
'timeZone': timezone,
}
}
def return_recurrence(self, doctype, doc):
e = frappe.get_doc(doctype, doc.name)
if e.repeat_till is not None:
end_date = datetime.combine(e.repeat_till, datetime.min.time()).strftime('UNTIL=%Y%m%dT%H%M%SZ')
else:
end_date = None
day = []
if e.repeat_on == "Every Day":
if e.monday is not None:
day.append("MO")
if e.tuesday is not None:
day.append("TU")
if e.wednesday is not None:
day.append("WE")
if e.thursday is not None:
day.append("TH")
if e.friday is not None:
day.append("FR")
if e.saturday is not None:
day.append("SA")
if e.sunday is not None:
day.append("SU")
day = "BYDAY=" + ",".join(str(d) for d in day)
frequency = "FREQ=DAILY"
elif e.repeat_on == "Every Week":
frequency = "FREQ=WEEKLY"
elif e.repeat_on == "Every Month":
frequency = "FREQ=MONTHLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1"
end_date = datetime.combine(add_days(e.repeat_till, 1), datetime.min.time()).strftime('UNTIL=%Y%m%dT%H%M%SZ')
elif e.repeat_on == "Every Year":
frequency = "FREQ=YEARLY"
else:
return None
wst = "WKST=SU"
elements = [frequency, end_date, wst, day]
return ";".join(str(e) for e in elements if e is not None and not not e)

View file

@ -40,7 +40,8 @@ class DataMigrationPlan(Document):
'fieldtype': 'Data',
'hidden': 1,
'read_only': 1,
'unique': 1
'unique': 1,
'no_copy': 1
}
for m in self.mappings:

View file

@ -77,6 +77,7 @@ class DataMigrationRun(Document):
def get_last_modified_condition(self):
last_run_timestamp = frappe.db.get_value('Data Migration Run', dict(
data_migration_plan=self.data_migration_plan,
data_migration_connector=self.data_migration_connector,
name=('!=', self.name)
), 'modified')
if last_run_timestamp:

View file

@ -0,0 +1,42 @@
# Google Calendar Integration
Frappe provides an integration with Google Calendar in order for all users to synchronize their events.
## Setup
In order to allow a synchronization with Google Calendar you need to connect to your application in Google Cloud Platform and then create an account for each of your users:
1. Create a new project on Google Cloud Platform and generate new OAuth 2.0 credentials
2. Add `https://{yoursite}` to Authorized JavaScript origins
3. Add `https://{yoursite}?cmd=frappe.integrations.doctype.gcalendar_settings.gcalendar_settings.google_callback` as an authorized redirect URI
4. Add your Client ID and Client Secret in the Gcalendar application: in "Google Calendar>GCalendar Settings"
Once this step is successfully completed, each user can create its account in "Google Calendar>GCalendar Account"
They will be requested to authorize your Google application to access their calendar information and will then be redirected to a success page.
## Features
1. Creation of a new calendar in Google Calendar
- Each user can choose a dedicated name for its Google Calendar.
2. Events synchronization from ERPNext to GCalendar
- All events created in ERPNext are created in Google Calendar.
- Recurring events are created as recurring events too.
- Events modified in ERPNext are updated in Google Calendar.
- Events deleted in ERPNext are deleted in Google Calendar.
3. Events synchronization from GCalendar to ERPNext
- Events created in Google Calendar are created in ERPNext.
- Events updated in Google Calendar are updated in ERPNext.
The synchronization module follows ERPNext's authorization rule:
an event will be only synchronized if it is public or if the user his the owner.
## Limitations
Currently, if an instance of a recurring event is cancelled in Google Calendar, this change will not be reflected in ERPNext.

View file

@ -134,7 +134,8 @@ scheduler_events = {
"frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.oauth.delete_oauth2_data",
"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment",
"frappe.twofactor.delete_all_barcodes_for_users"
"frappe.twofactor.delete_all_barcodes_for_users",
"frappe.integrations.doctype.gcalendar_settings.gcalendar_settings.sync"
],
"hourly": [
"frappe.model.utils.link_count.update_link_count",

View file

@ -0,0 +1,55 @@
{
"creation": "2017-12-19 14:42:54.264536",
"docstatus": 0,
"doctype": "Data Migration Mapping",
"fields": [
{
"is_child_table": 0,
"local_fieldname": "subject",
"remote_fieldname": "summary"
},
{
"is_child_table": 0,
"local_fieldname": "description",
"remote_fieldname": "description"
},
{
"is_child_table": 0,
"local_fieldname": "starts_on",
"remote_fieldname": "start_datetime"
},
{
"is_child_table": 0,
"local_fieldname": "ends_on",
"remote_fieldname": "end_datetime"
},
{
"is_child_table": 0,
"local_fieldname": "all_day",
"remote_fieldname": "all_day"
},
{
"is_child_table": 0,
"local_fieldname": "repeat_this_event",
"remote_fieldname": "repeat_this_event"
},
{
"is_child_table": 0,
"local_fieldname": "name",
"remote_fieldname": "name"
}
],
"idx": 0,
"local_doctype": "Event",
"local_primary_key": "gcalendar_sync_id",
"mapping_name": "Event to GCalendar",
"mapping_type": "Push",
"migration_id_field": "gcalendar_sync_id",
"modified": "2018-03-23 19:11:43.470602",
"modified_by": "Administrator",
"name": "Event to GCalendar",
"owner": "Administrator",
"page_length": 10,
"remote_objectname": "Events",
"remote_primary_key": "id"
}

View file

@ -0,0 +1,114 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import frappe
from datetime import datetime
from dateutil.parser import parse
from pytz import timezone
def pre_process(events):
if events["status"] == "cancelled":
if frappe.db.exists("Event", dict(gcalendar_sync_id=events["id"])):
e = frappe.get_doc("Event", dict(gcalendar_sync_id=events["id"]))
frappe.delete_doc("Event", e.name)
return {}
elif events["status"] == "confirmed":
if 'date' in events["start"]:
datevar = 'date'
else:
datevar = 'dateTime'
default_tz = frappe.db.get_value("System Settings", None, "time_zone")
event = {
'id': events["id"],
'summary': events["summary"],
'start_datetime': parse(events["start"][datevar]).astimezone(timezone(default_tz)),
'end_datetime': parse(events["end"][datevar]).astimezone(timezone(default_tz))
}
if "recurrence" in events:
recurrence = get_recurrence_event_fields_value(events['recurrence'][0], events["start"][datevar])
event.update(recurrence)
if 'description' in events:
event.update({'description': events["description"]})
else:
event.update({'description': ""})
if datevar == 'date':
event.update({'all_day': 1})
return event
def get_recurrence_event_fields_value(recur_rule, starts_on):
repeat_on = ""
repeat_till = ""
repeat_days = {}
# get recurrence rule from string
for _str in recur_rule.split(";"):
if "RRULE:FREQ" in _str:
repeat_every = _str.split("=")[1]
if repeat_every == "DAILY": repeat_on = "Every Day"
elif repeat_every == "WEEKLY": repeat_on = "Every Week"
elif repeat_every == "MONTHLY": repeat_on = "Every Month"
else: repeat_on = "Every Year"
elif "UNTIL" in _str:
# get repeat till
date = datetime.strptime(_str.split("=")[1], "%Y%m%dT%H%M%SZ")
repeat_till = get_repeat_till_date(date)
elif "COUNT" in _str:
# get repeat till
date = datetime.strptime(starts_on, "%Y-%m-%d %H:%M:%S")
repeat_till = get_repeat_till_date(date, count=_str.split("=")[1], repeat_on=repeat_on)
elif "BYDAY" in _str:
days = _str.split("=")[1]
if repeat_on == "DAILY":
repeat_days.update({
"sunday": 1 if "SU" in days else 0,
"monday": 1 if "MO" in days else 0,
"tuesday": 1 if "TU" in days else 0,
"wednesday": 1 if "WD" in days else 0,
"thursday": 1 if "TU" in days else 0,
"friday": 1 if "TU" in days else 0,
"saturday": 1 if "TU" in days else 0,
})
return {
"repeat_on": repeat_on,
"repeat_till": repeat_till,
"repeat_this_event": 1,
"repeat_days": repeat_days
}
def get_repeat_till_date(date, count=None, repeat_on=None):
if count:
if repeat_on == "Every Day":
# add days
date = date + timedelta(days=int(count))
elif repeat_on == "Every Week":
# add weeks
date = date + timedelta(weeks=int(count))
elif repeat_on == "Every Month":
# add months
date = add_months(date, int(count))
elif repeat_on == "Every Year":
# add years
date = add_months(date, int(count) * 12)
else:
# set default value
date = add_months(date, int(count))
return date.strftime("%Y-%m-%d")
def add_months(date, count):
import calendar
month = date.month - 1 + count
year = date.year + month / 12
month = month % 12 + 1
day = min(date.day,calendar.monthrange(year,month)[1])
return datetime(year,month,day)

View file

@ -0,0 +1,55 @@
{
"creation": "2018-02-16 13:07:13.325914",
"docstatus": 0,
"doctype": "Data Migration Mapping",
"fields": [
{
"is_child_table": 0,
"local_fieldname": "subject",
"remote_fieldname": "summary"
},
{
"is_child_table": 0,
"local_fieldname": "description",
"remote_fieldname": "description"
},
{
"is_child_table": 0,
"local_fieldname": "starts_on",
"remote_fieldname": "start_datetime"
},
{
"is_child_table": 0,
"local_fieldname": "ends_on",
"remote_fieldname": "end_datetime"
},
{
"is_child_table": 0,
"local_fieldname": "all_day",
"remote_fieldname": "all_day"
},
{
"is_child_table": 0,
"local_fieldname": "repeat_this_event",
"remote_fieldname": "repeat_this_event"
},
{
"is_child_table": 0,
"local_fieldname": "gcalendar_sync_id",
"remote_fieldname": "id"
}
],
"idx": 0,
"local_doctype": "Event",
"local_primary_key": "gcalendar_sync_id",
"mapping_name": "GCalendar to Event",
"mapping_type": "Pull",
"migration_id_field": "gcalendar_sync_id",
"modified": "2018-03-23 19:11:43.491367",
"modified_by": "Administrator",
"name": "GCalendar to Event",
"owner": "Administrator",
"page_length": 250,
"remote_objectname": "Events",
"remote_primary_key": "id"
}

View file

@ -0,0 +1,22 @@
{
"creation": "2018-03-23 19:10:23.715161",
"docstatus": 0,
"doctype": "Data Migration Plan",
"idx": 0,
"mappings": [
{
"enabled": 1,
"mapping": "Event to GCalendar"
},
{
"enabled": 1,
"mapping": "GCalendar to Event"
}
],
"modified": "2018-03-23 19:11:43.438560",
"modified_by": "Administrator",
"module": "Integrations",
"name": "GCalendar Sync",
"owner": "Administrator",
"plan_name": "GCalendar Sync"
}

View file

@ -0,0 +1,19 @@
// Copyright (c) 2018, DOKOS and contributors
// For license information, please see license.txt
frappe.ui.form.on('GCalendar Account', {
allow_google_access: function(frm) {
frappe.call({
method: "frappe.integrations.doctype.gcalendar_settings.gcalendar_settings.google_callback",
args: {
'account': frm.doc.name
},
callback: function(r) {
if(!r.exc) {
frm.save();
window.open(r.message.url);
}
}
});
}
});

View file

@ -0,0 +1,488 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:user",
"beta": 0,
"creation": "2018-02-13 09:42:24.068671",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "The name that will appear in Google Calendar",
"fieldname": "calendar_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Calendar Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "eval:doc.enabled",
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:!doc.__islocal",
"fieldname": "allow_google_access",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow GCalendar Access",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "refresh_token",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Refresh Token",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "authorization_code",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Authorization Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "session_token",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Session Token",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "state",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "State",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "gcalendar_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Google Calendar ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-03-23 19:29:46.887501",
"modified_by": "Administrator",
"module": "Integrations",
"name": "GCalendar Account",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, DOKOS and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class GCalendarAccount(Document):
def validate(self):
if self.enabled == 1:
self.create_google_connector()
def create_google_connector(self):
connector_name = 'Calendar Connector-' + self.name
if frappe.db.exists('Data Migration Connector', connector_name):
calendar_connector = frappe.get_doc('Data Migration Connector', connector_name)
calendar_connector.connector_type = 'Custom'
calendar_connector.python_module = 'frappe.data_migration.doctype.data_migration_connector.connectors.calendar_connector'
calendar_connector.username = self.name
calendar_connector.save()
return
frappe.get_doc({
'doctype': 'Data Migration Connector',
'connector_type': 'Custom',
'connector_name': connector_name,
'python_module': 'frappe.data_migration.doctype.data_migration_connector.connectors.calendar_connector',
'username': self.name
}).insert()

View file

@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: GCalendar Account", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new GCalendar Account
() => frappe.tests.make('GCalendar Account', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, DOKOS and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestGCalendarAccount(unittest.TestCase):
def test_create_connector(self):
users = frappe.get_all("User")
doc = frappe.new_doc("GCalendar Account")
doc.enabled = 1
doc.user = users[0].name
doc.calendar_name = "Frappe Test"
self.assertTrue(frappe.db.exists('GCalendar Account', users[0].name))
connector_name = 'Calendar Connector-' + users[0].name
self.assertTrue(frappe.db.exists('Data Migration Connector', connector_name))

View file

@ -0,0 +1,7 @@
// Copyright (c) 2017, DOKOS and contributors
// For license information, please see license.txt
frappe.ui.form.on('GCalendar Settings', {
});

View file

@ -0,0 +1,373 @@
{
"allow_copy": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-12-19 11:36:29.778694",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enable",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enable",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.enable",
"fieldname": "google_credentials",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Google API Credentials",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "client_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "client_secret",
"fieldtype": "Password",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client Secret",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "refresh_token",
"fieldtype": "Password",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Refresh Token",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "authorization_code",
"fieldtype": "Password",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Authorization Code",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "session_token",
"fieldtype": "Password",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Session Token",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "state",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "state",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-16 11:21:11.643750",
"modified_by": "Administrator",
"module": "Integrations",
"name": "GCalendar Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View file

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, DOKOS and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_request_site_address
import requests
import time
from frappe.utils.background_jobs import get_jobs
if frappe.conf.developer_mode:
import os
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
SCOPES = 'https://www.googleapis.com/auth/calendar'
AUTHORIZATION_BASE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
class GCalendarSettings(Document):
def sync(self):
"""Create and execute Data Migration Run for GCalendar Sync plan"""
frappe.has_permission('GCalendar Settings', throw=True)
accounts = frappe.get_all("GCalendar Account", filters={'enabled': 1})
queued_jobs = get_jobs(site=frappe.local.site, key='job_name')[frappe.local.site]
for account in accounts:
job_name = 'google_calendar_sync|{0}'.format(account.name)
if job_name not in queued_jobs:
frappe.enqueue('frappe.integrations.doctype.gcalendar_settings.gcalendar_settings.run_sync', queue='long', timeout=1500, job_name=job_name, account=account)
time.sleep(5)
def get_access_token(self):
if not self.refresh_token:
raise frappe.ValidationError(_("GCalendar is not configured."))
data = {
'client_id': self.client_id,
'client_secret': self.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:
frappe.throw(_("Something went wrong during the token generation. Please request again an authorization code."))
return r.get('access_token')
@frappe.whitelist()
def sync():
try:
gcalendar_settings = frappe.get_doc('GCalendar Settings')
if gcalendar_settings.enable == 1:
gcalendar_settings.sync()
except Exception:
frappe.log_error(frappe.get_traceback())
def run_sync(account):
exists = frappe.db.exists('Data Migration Run', dict(status=('in', ['Fail', 'Error'])))
if exists:
failed_run = frappe.get_doc("Data Migration Run", dict(status=('in', ['Fail', 'Error'])))
failed_run.delete()
started = frappe.db.exists('Data Migration Run', dict(status=('in', ['Started'])))
if started:
return
try:
doc = frappe.get_doc({
'doctype': 'Data Migration Run',
'data_migration_plan': 'GCalendar Sync',
'data_migration_connector': 'Calendar Connector-' + account.name
}).insert()
try:
doc.run()
except Exception:
frappe.log_error(frappe.get_traceback())
except Exception:
frappe.log_error(frappe.get_traceback())
@frappe.whitelist()
def google_callback(code=None, state=None, account=None):
redirect_uri = get_request_site_address(True) + "?cmd=frappe.integrations.doctype.gcalendar_settings.gcalendar_settings.google_callback"
if account is not None:
frappe.cache().hset("gcalendar_account","GCalendar Account", account)
doc = frappe.get_doc("GCalendar Settings")
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(doc.client_id, SCOPES, redirect_uri)
}
else:
try:
account = frappe.get_doc("GCalendar Account", frappe.cache().hget("gcalendar_account", "GCalendar Account"))
data = {'code': code,
'client_id': doc.client_id,
'client_secret': doc.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()
frappe.db.set_value("GCalendar Account", account.name, "authorization_code", code)
if 'access_token' in r:
frappe.db.set_value("GCalendar Account", account.name, "session_token", r['access_token'])
if 'refresh_token' in r:
frappe.db.set_value("GCalendar Account", account.name, "refresh_token", r['refresh_token'])
frappe.db.commit()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/integrations/gcalendar-success.html"
return
except Exception as e:
frappe.throw(e.message)
@frappe.whitelist()
def refresh_token(token):
if 'refresh_token' in token:
frappe.db.set_value("GCalendar Settings", None, "refresh_token", token['refresh_token'])
if 'access_token' in token:
frappe.db.set_value("GCalendar Settings", None, "session_token", token['access_token'])
frappe.db.commit()

View file

@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: GCalendar Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new GCalendar Settings
() => frappe.tests.make('GCalendar Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, DOKOS and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestGCalendarSettings(unittest.TestCase):
pass

View file

@ -0,0 +1,20 @@
{% extends "templates/web.html" %}
{% block title %}{{ _("Connection Success") }}{% endblock %}
{%- block page_content -%}
<div class='page-card'>
<div class='page-card-head'>
<span class='indicator green'>
{{ _("Success") }}</span>
</div>
<p>{{ _("Your connection request to Google Calendar was successfully accepted") }}</p>
<div><a href='{{ "/desk" }}' class='btn btn-primary btn-sm'>
{{ _("Back to Desk") }}</a></div>
</div>
<style>
.hero-and-content {
background-color: #f5f7fa;
}
</style>
{% endblock %}

View file

@ -48,4 +48,8 @@ googlemaps
mycli
braintree
future
faker
google-api-python-client
google-auth
google-auth-httplib2
google-auth-oauthlib
faker