[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:
parent
5307149a48
commit
68720c2a4f
26 changed files with 1695 additions and 19 deletions
|
|
@ -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"),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
42
frappe/docs/user/en/guides/integration/google_calendar.md
Normal file
42
frappe/docs/user/en/guides/integration/google_calendar.md
Normal 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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
0
frappe/integrations/data_migration_mapping/__init__.py
Normal file
0
frappe/integrations/data_migration_mapping/__init__.py
Normal 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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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))
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2017, DOKOS and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('GCalendar Settings', {
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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
|
||||
20
frappe/templates/pages/integrations/gcalendar-success.html
Normal file
20
frappe/templates/pages/integrations/gcalendar-success.html
Normal 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 %}
|
||||
|
|
@ -48,4 +48,8 @@ googlemaps
|
|||
mycli
|
||||
braintree
|
||||
future
|
||||
faker
|
||||
google-api-python-client
|
||||
google-auth
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib
|
||||
faker
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue