From 68720c2a4f72377ed3e742d6ec2935fff0b42dd6 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 6 Apr 2018 07:13:49 +0200 Subject: [PATCH] [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 --- frappe/config/integrations.py | 42 +- .../connectors/calendar_connector.py | 239 +++++++++ .../data_migration_plan.py | 3 +- .../data_migration_run/data_migration_run.py | 1 + .../en/guides/integration/google_calendar.md | 42 ++ frappe/hooks.py | 3 +- .../data_migration_mapping/__init__.py | 0 .../event_to_gcalendar/__init__.py | 0 .../event_to_gcalendar.json | 55 ++ .../gcalendar_to_event/__init__.py | 114 ++++ .../gcalendar_to_event.json | 55 ++ .../gcalendar_sync/gcalendar_sync.json | 22 + .../doctype/gcalendar_account/__init__.py | 0 .../gcalendar_account/gcalendar_account.js | 19 + .../gcalendar_account/gcalendar_account.json | 488 ++++++++++++++++++ .../gcalendar_account/gcalendar_account.py | 30 ++ .../test_gcalendar_account.js | 23 + .../test_gcalendar_account.py | 19 + .../doctype/gcalendar_settings/__init__.py | 0 .../gcalendar_settings/gcalendar_settings.js | 7 + .../gcalendar_settings.json | 373 +++++++++++++ .../gcalendar_settings/gcalendar_settings.py | 121 +++++ .../test_gcalendar_settings.js | 23 + .../test_gcalendar_settings.py | 9 + .../pages/integrations/gcalendar-success.html | 20 + requirements.txt | 6 +- 26 files changed, 1695 insertions(+), 19 deletions(-) create mode 100644 frappe/data_migration/doctype/data_migration_connector/connectors/calendar_connector.py create mode 100644 frappe/docs/user/en/guides/integration/google_calendar.md create mode 100644 frappe/integrations/data_migration_mapping/__init__.py create mode 100644 frappe/integrations/data_migration_mapping/event_to_gcalendar/__init__.py create mode 100644 frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json create mode 100644 frappe/integrations/data_migration_mapping/gcalendar_to_event/__init__.py create mode 100644 frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json create mode 100644 frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json create mode 100644 frappe/integrations/doctype/gcalendar_account/__init__.py create mode 100644 frappe/integrations/doctype/gcalendar_account/gcalendar_account.js create mode 100644 frappe/integrations/doctype/gcalendar_account/gcalendar_account.json create mode 100644 frappe/integrations/doctype/gcalendar_account/gcalendar_account.py create mode 100644 frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js create mode 100644 frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py create mode 100644 frappe/integrations/doctype/gcalendar_settings/__init__.py create mode 100644 frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js create mode 100644 frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json create mode 100644 frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py create mode 100644 frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js create mode 100644 frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py create mode 100644 frappe/templates/pages/integrations/gcalendar-success.html diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 1b5a00a9dc..48017ab75c 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -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"), } ] } diff --git a/frappe/data_migration/doctype/data_migration_connector/connectors/calendar_connector.py b/frappe/data_migration/doctype/data_migration_connector/connectors/calendar_connector.py new file mode 100644 index 0000000000..c6f35b9a5d --- /dev/null +++ b/frappe/data_migration/doctype/data_migration_connector/connectors/calendar_connector.py @@ -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) diff --git a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py index 313ca5943e..decd27f913 100644 --- a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py +++ b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py @@ -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: diff --git a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py index f37ead9e0d..774b00bbbc 100644 --- a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py +++ b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py @@ -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: diff --git a/frappe/docs/user/en/guides/integration/google_calendar.md b/frappe/docs/user/en/guides/integration/google_calendar.md new file mode 100644 index 0000000000..e77a89cb45 --- /dev/null +++ b/frappe/docs/user/en/guides/integration/google_calendar.md @@ -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. diff --git a/frappe/hooks.py b/frappe/hooks.py index 5c49d5661c..d1f7f0e437 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -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", diff --git a/frappe/integrations/data_migration_mapping/__init__.py b/frappe/integrations/data_migration_mapping/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/data_migration_mapping/event_to_gcalendar/__init__.py b/frappe/integrations/data_migration_mapping/event_to_gcalendar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json b/frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json new file mode 100644 index 0000000000..eaaa631f08 --- /dev/null +++ b/frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json @@ -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" +} \ No newline at end of file diff --git a/frappe/integrations/data_migration_mapping/gcalendar_to_event/__init__.py b/frappe/integrations/data_migration_mapping/gcalendar_to_event/__init__.py new file mode 100644 index 0000000000..f0283b2c75 --- /dev/null +++ b/frappe/integrations/data_migration_mapping/gcalendar_to_event/__init__.py @@ -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) diff --git a/frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json b/frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json new file mode 100644 index 0000000000..cbdc90e149 --- /dev/null +++ b/frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json @@ -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" +} \ No newline at end of file diff --git a/frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json b/frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json new file mode 100644 index 0000000000..c3a5ec0e67 --- /dev/null +++ b/frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json @@ -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" +} \ No newline at end of file diff --git a/frappe/integrations/doctype/gcalendar_account/__init__.py b/frappe/integrations/doctype/gcalendar_account/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.js b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.js new file mode 100644 index 0000000000..d8ad7d46ad --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.js @@ -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); + } + } + }); + } +}); diff --git a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.json b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.json new file mode 100644 index 0000000000..2ffb3b51f8 --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.json @@ -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 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.py b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.py new file mode 100644 index 0000000000..bc2647dd49 --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.py @@ -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() diff --git a/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js b/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js new file mode 100644 index 0000000000..580b240c49 --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js @@ -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() + ]); + +}); diff --git a/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py b/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py new file mode 100644 index 0000000000..535c9c5467 --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py @@ -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)) diff --git a/frappe/integrations/doctype/gcalendar_settings/__init__.py b/frappe/integrations/doctype/gcalendar_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js new file mode 100644 index 0000000000..3be5603b9b --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js @@ -0,0 +1,7 @@ +// Copyright (c) 2017, DOKOS and contributors +// For license information, please see license.txt + +frappe.ui.form.on('GCalendar Settings', { + + +}); diff --git a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json new file mode 100644 index 0000000000..05111ec791 --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json @@ -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 +} diff --git a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py new file mode 100644 index 0000000000..0eff2b0f98 --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py @@ -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() diff --git a/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js b/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js new file mode 100644 index 0000000000..23bd41ff9b --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js @@ -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() + ]); + +}); diff --git a/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py b/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py new file mode 100644 index 0000000000..cbc42a1c5f --- /dev/null +++ b/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py @@ -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 diff --git a/frappe/templates/pages/integrations/gcalendar-success.html b/frappe/templates/pages/integrations/gcalendar-success.html new file mode 100644 index 0000000000..b6d22300c0 --- /dev/null +++ b/frappe/templates/pages/integrations/gcalendar-success.html @@ -0,0 +1,20 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Connection Success") }}{% endblock %} + +{%- block page_content -%} +
+
+ + {{ _("Success") }} +
+

{{ _("Your connection request to Google Calendar was successfully accepted") }}

+
+ {{ _("Back to Desk") }}
+
+ +{% endblock %} diff --git a/requirements.txt b/requirements.txt index a6d5835bec..884acfd59d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,4 +48,8 @@ googlemaps mycli braintree future -faker \ No newline at end of file +google-api-python-client +google-auth +google-auth-httplib2 +google-auth-oauthlib +faker