Merge branch 'version-12-hotfix' into version-12

This commit is contained in:
Sahil Khan 2019-08-13 14:43:14 +05:30
commit aa6ca43a74
58 changed files with 1001 additions and 2523 deletions

View file

@ -23,7 +23,7 @@ if sys.version[0] == '2':
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '12.0.5'
__version__ = '12.0.6'
__title__ = "Frappe Framework"
local = Local()

View file

@ -30,13 +30,13 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type = db_type)
db_type=db_type)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None,force=False,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None):
"""Install a new Frappe site"""
@ -67,7 +67,7 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql,force=force, reinstall=reinstall, db_type=db_type)
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:

View file

@ -92,16 +92,6 @@ def get_data():
"name": "Google Settings",
"description": _("Google API Settings."),
},
{
"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",
@ -116,6 +106,11 @@ def get_data():
"type": "doctype",
"name": "Google Contacts",
"description": _("Google Contacts Integration."),
},
{
"type": "doctype",
"name": "Google Calendar",
"description": _("Google Calendar Integration."),
}
]
}

View file

@ -95,6 +95,6 @@ class TestDataImport(unittest.TestCase):
exporter.export_data("Event", all_doctypes=True, template=True, file_type="Excel")
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
content = read_xlsx_file_from_attached_file(fcontent=frappe.response.filecontent)
content.append(["", "_test", "Private", "05-11-2017 13:51:48", "Event", "0", "0", "", "1", "", "", 0, 0, 0, 0, 0, 0, 0, "blue"])
content.append(["", "_test", "Private", "05-11-2017 13:51:48", "Event", "blue", "0", "0", "", "Open", "", 0, "", 0, "", "", "1", 0, "", "", 0, 0, 0, 0, 0, 0, 0])
importer.upload(content)
self.assertTrue(frappe.db.get_value("Event", {"subject": "_test"}, "name"))

View file

@ -29,7 +29,7 @@ from frappe.utils.nestedset import NestedSet
from frappe.utils import strip
from PIL import Image, ImageOps
from six import StringIO, string_types
from six.moves.urllib.parse import unquote
from six.moves.urllib.parse import unquote, quote
from six import text_type, PY2
import zipfile
@ -78,7 +78,7 @@ class File(NestedSet):
self.add_comment_in_reference_doc('Attachment',
_('Added {0}').format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
"icon": ' <i class="fa fa-lock text-warning"></i>' if self.is_private else "",
"file_url": self.file_url.replace("#", "%23") if self.file_name else self.file_url,
"file_url": quote(self.file_url) if self.file_url else self.file_name,
"file_name": self.file_name or self.file_url
})))

View file

@ -125,7 +125,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len,
linked_doctypes += [doctype]
if txt:
linked_doctypes = [d for d in linked_doctypes if txt in d.lower()]
linked_doctypes = [d for d in linked_doctypes if txt.lower() in d.lower()]
linked_doctypes.sort()

View file

@ -26,9 +26,9 @@ class TestVersion(unittest.TestCase):
diff = get_diff(old_doc, new_doc)['changed']
self.assertEqual(get_fieldnames(diff)[0], 'starts_on')
self.assertEqual(get_old_values(diff)[0], '01-01-2014 00:00:00')
self.assertEqual(get_new_values(diff)[0], '07-20-2017 00:00:00')
self.assertEqual(get_fieldnames(diff)[1], 'starts_on')
self.assertEqual(get_old_values(diff)[1], '01-01-2014 00:00:00')
self.assertEqual(get_new_values(diff)[1], '07-20-2017 00:00:00')
def get_fieldnames(change_array):
return [d[0] for d in change_array]

View file

@ -46,7 +46,7 @@ class TestCustomizeForm(unittest.TestCase):
d = self.get_customize_form("Event")
self.assertEquals(d.doc_type, "Event")
self.assertEquals(len(d.get("fields")), 29)
self.assertEquals(len(d.get("fields")), 35)
d = self.get_customize_form("Event")
self.assertEquals(d.doc_type, "Event")

View file

@ -1,249 +0,0 @@
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, add_years
from frappe.desk.doctype.event.event import has_permission
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':
d = frappe.get_doc("Event", doc["name"])
if has_permission(d, self.account.name):
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':
d = frappe.get_doc("Event", doc["name"])
if has_permission(d, self.account.name):
if 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 = []
events = {"items": []}
while True:
try:
events = self.gcalendar.events().list(calendarId=self.account.gcalendar_id, maxResults=page_length,
singleEvents=False, showDeleted=True, syncToken=self.account.next_sync_token or None).execute()
except HttpError as err:
if err.resp.status in [410]:
events = self.gcalendar.events().list(calendarId=self.account.gcalendar_id, maxResults=page_length,
singleEvents=False, showDeleted=True, timeMin=add_years(None, -1).strftime('%Y-%m-%dT%H:%M:%SZ')).execute()
else:
frappe.log_error(err.resp, "GCalendar Events Fetch Error")
for event in events['items']:
event.update({'account': self.account.name})
event.update({'calendar_tz': events['timeZone']})
results.append(event)
page_token = events.get('nextPageToken')
if not page_token:
if events.get('nextSyncToken'):
frappe.db.set_value("GCalendar Account", self.connector.username, "next_sync_token", events.get('nextSyncToken'))
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': add_days(doc.end_datetime.date(), 1).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 == "Daily":
frequency = "FREQ=DAILY"
elif e.repeat_on == "Weekly":
if e.monday == 1:
day.append("MO")
if e.tuesday == 1:
day.append("TU")
if e.wednesday == 1:
day.append("WE")
if e.thursday == 1:
day.append("TH")
if e.friday == 1:
day.append("FR")
if e.saturday == 1:
day.append("SA")
if e.sunday == 1:
day.append("SU")
day = "BYDAY=" + ",".join(str(d) for d in day)
frequency = "FREQ=WEEKLY"
elif e.repeat_on == "Monthly":
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 == "Yearly":
frequency = "FREQ=YEARLY"
else:
return None
wst = "WKST=SU"
elements = [frequency, end_date, wst, day]
return ";".join(str(e) for e in elements if e is not None and not not e)

View file

@ -65,7 +65,7 @@ class PostgresDatabase(Database):
def get_connection(self):
# warnings.filterwarnings('ignore', category=psycopg2.Warning)
conn = psycopg2.connect('host={} dbname={} user={} password={} port={}'.format(
conn = psycopg2.connect("host='{}' dbname='{}' user='{}' password='{}' port={}".format(
self.host, self.user, self.user, self.password, self.port
))
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) # TODO: Remove this

View file

@ -10,7 +10,14 @@ frappe.ui.form.on("Event", {
"issingle": 0,
}
};
})
});
frm.set_query('google_calendar', function() {
return {
filters: {
"owner": frappe.session.user
}
};
});
},
refresh: function(frm) {
if(frm.doc.event_participants) {

View file

@ -10,6 +10,7 @@
"subject",
"event_category",
"event_type",
"color",
"send_reminder",
"repeat_this_event",
"column_break_4",
@ -17,6 +18,13 @@
"ends_on",
"status",
"all_day",
"sync_with_google_calendar",
"sb_00",
"google_calendar",
"pulled_from_google_calendar",
"cb_00",
"google_calendar_id",
"google_calendar_event_id",
"section_break_13",
"repeat_on",
"repeat_till",
@ -29,8 +37,6 @@
"saturday",
"sunday",
"section_break_8",
"color",
"section_break_6",
"description",
"participants",
"event_participants"
@ -39,6 +45,7 @@
{
"fieldname": "details",
"fieldtype": "Section Break",
"label": "Details",
"oldfieldtype": "Section Break"
},
{
@ -182,10 +189,6 @@
"fieldtype": "Color",
"label": "Color"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
@ -216,11 +219,54 @@
"in_standard_filter": 1,
"label": "Status",
"options": "Open\nClosed"
},
{
"collapsible": 1,
"depends_on": "eval:doc.sync_with_google_calendar",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "Google Calendar"
},
{
"fetch_from": "google_calendar.google_calendar_id",
"fieldname": "google_calendar_id",
"fieldtype": "Data",
"label": "Google Calendar ID",
"read_only": 1
},
{
"fieldname": "cb_00",
"fieldtype": "Column Break"
},
{
"fieldname": "google_calendar_event_id",
"fieldtype": "Data",
"label": "Google Calendar Event ID",
"read_only": 1
},
{
"default": "0",
"fieldname": "sync_with_google_calendar",
"fieldtype": "Check",
"label": "Sync with Google Calendar"
},
{
"fieldname": "google_calendar",
"fieldtype": "Link",
"label": "Google Calendar",
"options": "Google Calendar"
},
{
"default": "0",
"fieldname": "pulled_from_google_calendar",
"fieldtype": "Check",
"label": "Pulled from Google Calendar",
"read_only": 1
}
],
"icon": "fa fa-calendar",
"idx": 1,
"modified": "2019-07-27 10:33:39.183832",
"modified": "2019-08-08 16:01:19.489396",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",

View file

@ -31,6 +31,9 @@ class Event(Document):
if self.repeat_on == "Daily" and self.ends_on and getdate(self.starts_on) != getdate(self.ends_on):
frappe.throw(_("Daily Events should finish on the Same Day."))
if self.sync_with_google_calendar and not self.google_calendar:
frappe.throw(_("Select Google Calendar to which event should be synced."))
def on_update(self):
self.sync_communication()

View file

@ -141,6 +141,11 @@ doc_events = {
},
"Email Group Member": {
"validate": "frappe.email.doctype.email_group.email_group.restrict_email_group"
},
"Event": {
"after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar",
"on_update": "frappe.integrations.doctype.google_calendar.google_calendar.update_event_in_google_calendar",
"on_trash": "frappe.integrations.doctype.google_calendar.google_calendar.delete_event_from_google_calendar",
}
}
@ -152,7 +157,6 @@ scheduler_events = {
"frappe.oauth.delete_oauth2_data",
"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment",
"frappe.twofactor.delete_all_barcodes_for_users",
"frappe.integrations.doctype.gcalendar_settings.gcalendar_settings.sync",
"frappe.website.doctype.web_page.web_page.check_publish_status",
'frappe.utils.global_search.sync_global_search'
],
@ -165,6 +169,7 @@ scheduler_events = {
"frappe.limits.update_site_usage",
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates",
"frappe.integrations.doctype.google_calendar.google_calendar.sync"
],
"daily": [
"frappe.email.queue.clear_outbox",

View file

@ -26,7 +26,6 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
make_conf(db_name, site_config=site_config, db_type=db_type)
frappe.flags.in_install_db = True
@ -44,7 +43,6 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
frappe.flags.in_install_db = False
def install_app(name, verbose=False, set_as_patched=True):
frappe.flags.in_install = name
frappe.flags.ignore_in_install = False

View file

@ -1,100 +0,0 @@
{
"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": "repeat_on",
"remote_fieldname": "repeat_on"
},
{
"is_child_table": 0,
"local_fieldname": "repeat_till",
"remote_fieldname": "repeat_till"
},
{
"is_child_table": 0,
"local_fieldname": "monday",
"remote_fieldname": "monday"
},
{
"is_child_table": 0,
"local_fieldname": "tuesday",
"remote_fieldname": "tuesday"
},
{
"is_child_table": 0,
"local_fieldname": "wednesday",
"remote_fieldname": "wednesday"
},
{
"is_child_table": 0,
"local_fieldname": "thursday",
"remote_fieldname": "thursday"
},
{
"is_child_table": 0,
"local_fieldname": "friday",
"remote_fieldname": "friday"
},
{
"is_child_table": 0,
"local_fieldname": "saturday",
"remote_fieldname": "saturday"
},
{
"is_child_table": 0,
"local_fieldname": "sunday",
"remote_fieldname": "sunday"
},
{
"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": "2019-03-26 10:16:45.400150",
"modified_by": "Administrator",
"name": "Event to GCalendar",
"owner": "Administrator",
"page_length": 10,
"remote_objectname": "Events",
"remote_primary_key": "id"
}

View file

@ -1,140 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import frappe
from datetime import datetime, timedelta
from dateutil.parser import parse
from pytz import timezone
from frappe.utils import add_days
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'
start_dt = parse(events["start"]['date'])
end_dt = add_days(parse(events["end"]['date']), -1)
elif 'dateTime' in events["start"]:
datevar = 'dateTime'
start_dt = parse(events["start"]['dateTime'])
end_dt = parse(events["end"]['dateTime'])
if start_dt.tzinfo is None or start_dt.tzinfo.utcoffset(start_dt) is None:
if "timeZone" in events["start"]:
event_tz = events["start"]["timeZone"]
else:
event_tz = events["calendar_tz"]
start_dt = timezone(event_tz).localize(start_dt)
if end_dt.tzinfo is None or end_dt.tzinfo.utcoffset(end_dt) is None:
if "timeZone" in events["end"]:
event_tz = events["end"]["timeZone"]
else:
event_tz = events["calendar_tz"]
end_dt = timezone(event_tz).localize(end_dt)
default_tz = frappe.db.get_value("System Settings", None, "time_zone")
event = {
'id': events["id"],
'summary': events["summary"],
'start_datetime': start_dt.astimezone(timezone(default_tz)).strftime('%Y-%m-%d %H:%M:%S'),
'end_datetime': end_dt.astimezone(timezone(default_tz)).strftime('%Y-%m-%d %H:%M:%S'),
'account': events['account']
}
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 = "Daily"
elif repeat_every == "WEEKLY": repeat_on = "Weekly"
elif repeat_every == "MONTHLY": repeat_on = "Monthly"
else: repeat_on = "Yearly"
elif "UNTIL" in _str:
# get repeat till
date = parse(_str.split("=")[1])
repeat_till = get_repeat_till_date(date)
elif "COUNT" in _str:
# get repeat till
date = parse(starts_on)
repeat_till = get_repeat_till_date(date, count=_str.split("=")[1], repeat_on=repeat_on)
elif "BYDAY" in _str:
days = _str.split("=")[1]
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 "TH" in days else 0,
"friday": 1 if "FR" in days else 0,
"saturday": 1 if "SA" in days else 0,
})
repeat_on = "Weekly"
recurrence = {
"repeat_on": repeat_on,
"repeat_till": repeat_till,
"repeat_this_event": 1
}
if repeat_days:
recurrence.update(repeat_days)
return recurrence
def get_repeat_till_date(date, count=None, repeat_on=None):
if count:
if repeat_on == "Daily":
# add days
date = date + timedelta(days=int(count))
elif repeat_on == "Weekly":
# add weeks
date = date + timedelta(weeks=int(count))
elif repeat_on == "Monthly":
# add months
date = add_months(date, int(count))
elif repeat_on == "Yearly":
# add years
date = add_months(date, int(count) * 12)
else:
# set default value
date = add_months(date, int(count))
return date.strftime("%Y-%m-%d")
def add_months(date, count):
import calendar
month = date.month - 1 + count
year = date.year + month / 12
month = month % 12 + 1
day = min(date.day,calendar.monthrange(year,month)[1])
return datetime(year,month,day)

View file

@ -1,105 +0,0 @@
{
"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": "repeat_on",
"remote_fieldname": "repeat_on"
},
{
"is_child_table": 0,
"local_fieldname": "repeat_till",
"remote_fieldname": "repeat_till"
},
{
"is_child_table": 0,
"local_fieldname": "monday",
"remote_fieldname": "monday"
},
{
"is_child_table": 0,
"local_fieldname": "tuesday",
"remote_fieldname": "tuesday"
},
{
"is_child_table": 0,
"local_fieldname": "wednesday",
"remote_fieldname": "wednesday"
},
{
"is_child_table": 0,
"local_fieldname": "thursday",
"remote_fieldname": "thursday"
},
{
"is_child_table": 0,
"local_fieldname": "friday",
"remote_fieldname": "friday"
},
{
"is_child_table": 0,
"local_fieldname": "saturday",
"remote_fieldname": "saturday"
},
{
"is_child_table": 0,
"local_fieldname": "sunday",
"remote_fieldname": "sunday"
},
{
"is_child_table": 0,
"local_fieldname": "gcalendar_sync_id",
"remote_fieldname": "id"
},
{
"is_child_table": 0,
"local_fieldname": "owner",
"remote_fieldname": "account"
}
],
"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": "2019-03-26 10:16:45.426138",
"modified_by": "Administrator",
"name": "GCalendar to Event",
"owner": "Administrator",
"page_length": 250,
"remote_objectname": "Events",
"remote_primary_key": "id"
}

View file

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

View file

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

View file

@ -1,533 +0,0 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 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_in_quick_entry": 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_in_quick_entry": 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": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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_in_quick_entry": 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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "next_sync_token",
"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": "Next Sync 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
}
],
"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-10-04 13:32:27.673907",
"modified_by": "Administrator",
"module": "Integrations",
"name": "GCalendar Account",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 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,
"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,
"track_views": 0
}

View file

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

View file

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

View file

@ -1,20 +0,0 @@
# -*- 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"
doc.save()
self.assertTrue(frappe.db.exists('GCalendar Account', users[0].name))
connector_name = 'Calendar Connector-' + users[0].name
self.assertTrue(frappe.db.exists('Data Migration Connector', connector_name))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,58 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Google Calendar", {
refresh: function(frm) {
if (frm.is_new()) {
frm.dashboard.set_headline(__("To use Google Calendar, enable <a href='#Form/Google Settings'>Google Settings</a>."));
}
frappe.realtime.on("import_google_calendar", (data) => {
if (data.progress) {
frm.dashboard.show_progress("Syncing Google Calendar", data.progress / data.total * 100,
__("Syncing {0} of {1}", [data.progress, data.total]));
if (data.progress === data.total) {
frm.dashboard.hide_progress("Syncing Google Calendar");
}
}
});
if (frm.doc.refresh_token) {
frm.add_custom_button(__("Sync Calendar"), function () {
frappe.show_alert({
indicator: "green",
message: __("Syncing")
});
frappe.call({
method: "frappe.integrations.doctype.google_calendar.google_calendar.sync",
args: {
"g_calendar": frm.doc.name
},
}).then((r) => {
frappe.hide_progress();
frappe.msgprint(r.message);
});
});
}
},
authorize_google_calendar_access: function(frm) {
let reauthorize = 0;
if(frm.doc.authorization_code) {
reauthorize = 1;
}
frappe.call({
method: "frappe.integrations.doctype.google_calendar.google_calendar.authorize_access",
args: {
"g_calendar": frm.doc.name,
"reauthorize": reauthorize
},
callback: function(r) {
if(!r.exc) {
frm.save();
window.open(r.message.url);
}
}
});
}
});

View file

@ -0,0 +1,146 @@
{
"autoname": "field:calendar_name",
"creation": "2019-07-06 17:54:09.450100",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable",
"sb_00",
"calendar_name",
"user",
"authorize_google_calendar_access",
"sb_01",
"pull_from_google_calendar",
"cb_01",
"push_to_google_calendar",
"section_break_3",
"google_calendar_id",
"refresh_token",
"authorization_code",
"next_sync_token"
],
"fields": [
{
"default": "1",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "eval: doc.enable",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "Google Calendar"
},
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1
},
{
"description": "The name that will appear in Google Calendar",
"fieldname": "calendar_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Calendar Name",
"reqd": 1,
"unique": 1
},
{
"depends_on": "eval: doc.enable",
"fieldname": "section_break_3",
"fieldtype": "Section Break"
},
{
"fieldname": "refresh_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Refresh Token"
},
{
"fieldname": "authorization_code",
"fieldtype": "Password",
"hidden": 1,
"label": "Authorization Code"
},
{
"fieldname": "next_sync_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Next Sync Token"
},
{
"fieldname": "google_calendar_id",
"fieldtype": "Data",
"label": "Google Calendar ID",
"read_only": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "authorize_google_calendar_access",
"fieldtype": "Button",
"label": "Authorize Google Calendar Access"
},
{
"depends_on": "eval: doc.enable",
"fieldname": "sb_01",
"fieldtype": "Section Break",
"label": "Sync"
},
{
"default": "1",
"fieldname": "pull_from_google_calendar",
"fieldtype": "Check",
"label": "Pull from Google Calendar"
},
{
"fieldname": "cb_01",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "push_to_google_calendar",
"fieldtype": "Check",
"label": "Push to Google Calendar"
}
],
"modified": "2019-08-08 15:44:15.798362",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Calendar",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,594 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import requests
import googleapiclient.discovery
import google.oauth2.credentials
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_request_site_address
from googleapiclient.errors import HttpError
from frappe.utils import add_days, get_datetime, get_weekdays, now_datetime, add_to_date, get_time_zone
from dateutil import parser
from datetime import datetime, timedelta
from six.moves.urllib.parse import quote
SCOPES = "https://www.googleapis.com/auth/calendar"
google_calendar_frequencies = {
"RRULE:FREQ=DAILY": "Daily",
"RRULE:FREQ=WEEKLY": "Weekly",
"RRULE:FREQ=MONTHLY": "Monthly",
"RRULE:FREQ=YEARLY": "Yearly"
}
google_calendar_days = {
"MO": "monday",
"TU": "tuesday",
"WE": "wednesday",
"TH": "thursday",
"FR": "friday",
"SA": "saturday",
"SU": "sunday"
}
framework_frequencies = {
"Daily": "RRULE:FREQ=DAILY;",
"Weekly": "RRULE:FREQ=WEEKLY;",
"Monthly": "RRULE:FREQ=MONTHLY;",
"Yearly": "RRULE:FREQ=YEARLY;"
}
framework_days = {
"monday": "MO",
"tuesday": "TU",
"wednesday": "WE",
"thursday": "TH",
"friday": "FR",
"saturday": "SA",
"sunday": "SU"
}
class GoogleCalendar(Document):
def validate(self):
google_settings = frappe.get_single("Google Settings")
if not google_settings.enable:
frappe.throw(_("Enable Google API in Google Settings."))
if not google_settings.client_id or not google_settings.client_secret:
frappe.throw(_("Enter Client Id and Client Secret in Google Settings."))
return google_settings
def get_access_token(self):
google_settings = self.validate()
if not self.refresh_token:
button_label = frappe.bold(_("Allow Google Calendar Access"))
raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label))
data = {
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
"refresh_token": self.get_password(fieldname="refresh_token", raise_exception=False),
"grant_type": "refresh_token",
"scope": SCOPES
}
try:
r = requests.post("https://www.googleapis.com/oauth2/v4/token", data=data).json()
except requests.exceptions.HTTPError:
button_label = frappe.bold(_("Allow Google Calendar Access"))
frappe.throw(_("Something went wrong during the token generation. Click on {0} to generate a new one.").format(button_label))
return r.get("access_token")
@frappe.whitelist()
def authorize_access(g_calendar, reauthorize=None):
"""
If no Authorization code get it from Google and then request for Refresh Token.
Google Calendar Name is set to flags to set_value after Authorization Code is obtained.
"""
google_settings = frappe.get_doc("Google Settings")
google_calendar = frappe.get_doc("Google Calendar", g_calendar)
redirect_uri = get_request_site_address(True) + "?cmd=frappe.integrations.doctype.google_calendar.google_calendar.google_callback"
if not google_calendar.authorization_code or reauthorize:
frappe.cache().hset("google_calendar", "google_calendar", google_calendar.name)
return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri)
else:
try:
data = {
"code": google_calendar.get_password(fieldname="authorization_code", raise_exception=False),
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
"redirect_uri": redirect_uri,
"grant_type": "authorization_code"
}
r = requests.post("https://www.googleapis.com/oauth2/v4/token", data=data).json()
if "refresh_token" in r:
frappe.db.set_value("Google Calendar", google_calendar.name, "refresh_token", r.get("refresh_token"))
frappe.db.commit()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/desk#Form/{0}/{1}".format(quote("Google Calendar"), quote(google_calendar.name))
frappe.msgprint(_("Google Calendar has been configured."))
except Exception as e:
frappe.throw(e)
def get_authentication_url(client_id=None, redirect_uri=None):
return {
"url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri)
}
@frappe.whitelist()
def google_callback(code=None):
"""
Authorization code is sent to callback as per the API configuration
"""
google_calendar = frappe.cache().hget("google_calendar", "google_calendar")
frappe.db.set_value("Google Calendar", google_calendar, "authorization_code", code)
frappe.db.commit()
authorize_access(google_calendar)
@frappe.whitelist()
def sync(g_calendar=None):
filters = {"enable": 1}
if g_calendar:
filters.update({"name": g_calendar})
google_calendars = frappe.get_list("Google Calendar", filters=filters)
for g in google_calendars:
return sync_events_from_google_calendar(g.name)
def get_google_calendar_object(g_calendar):
"""
Returns an object of Google Calendar along with Google Calendar doc.
"""
google_settings = frappe.get_doc("Google Settings")
account = frappe.get_doc("Google Calendar", g_calendar)
credentials_dict = {
"token": account.get_access_token(),
"refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False),
"token_uri": "https://www.googleapis.com/oauth2/v4/token",
"client_id": google_settings.client_id,
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
"scopes": "https://www.googleapis.com/auth/calendar/v3"
}
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
google_calendar = googleapiclient.discovery.build("calendar", "v3", credentials=credentials)
check_google_calendar(account, google_calendar)
account.load_from_db()
return google_calendar, account
def check_google_calendar(account, google_calendar):
"""
Checks if Google Calendar is present with the specified name.
If not, creates one.
"""
account.load_from_db()
try:
if account.google_calendar_id:
google_calendar.calendars().get(calendarId=account.google_calendar_id).execute()
else:
# If no Calendar ID create a new Calendar
calendar = {
"summary": account.calendar_name,
"timeZone": frappe.db.get_single_value("System Settings", "time_zone")
}
created_calendar = google_calendar.calendars().insert(body=calendar).execute()
frappe.db.set_value("Google Calendar", account.name, "google_calendar_id", created_calendar.get("id"))
frappe.db.commit()
except HttpError as err:
frappe.throw(_("Google Calendar - Could not create Calendar for {0}, error code {1}.").format(account.name, err.resp.status))
def sync_events_from_google_calendar(g_calendar, method=None, page_length=10):
"""
Syncs Events from Google Calendar in Framework Calendar.
Google Calendar returns nextSyncToken when all the events in Google Calendar are fetched.
nextSyncToken is returned at the very last page
https://developers.google.com/calendar/v3/sync
"""
google_calendar, account = get_google_calendar_object(g_calendar)
if not account.pull_from_google_calendar:
return
results = []
while True:
try:
# API Response listed at EOF
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
events = google_calendar.events().list(calendarId=account.google_calendar_id, maxResults=page_length,
singleEvents=False, showDeleted=True, syncToken=sync_token).execute()
except HttpError as err:
frappe.throw(_("Google Calendar - Could not fetch event from Google Calendar, error code {0}.").format(err.resp.status))
for event in events.get("items"):
results.append(event)
if not events.get("nextPageToken"):
if events.get("nextSyncToken"):
frappe.db.set_value("Google Calendar", account.name, "next_sync_token", events.get("nextSyncToken"))
frappe.db.commit()
break
for idx, event in enumerate(results):
frappe.publish_realtime("import_google_calendar", dict(progress=idx+1, total=len(results)), user=frappe.session.user)
# If Google Calendar Event if confirmed, then create an Event
if event.get("status") == "confirmed":
recurrence = None
if event.get("recurrence"):
try:
recurrence = event.get("recurrence")[0]
except IndexError:
pass
if not frappe.db.exists("Event", {"google_calendar_event_id": event.get("id")}):
insert_event_to_calendar(account, event, recurrence)
else:
update_event_in_calendar(account, event, recurrence)
elif event.get("status") == "cancelled":
# If any synced Google Calendar Event is cancelled, then close the Event
frappe.db.set_value("Event", {"google_calendar_id": account.google_calendar_id, "google_calendar_event_id": event.get("id")}, "status", "Closed")
frappe.get_doc({
"doctype": "Comment",
"comment_type": "Info",
"reference_doctype": "Event",
"reference_name": frappe.db.get_value("Event", {"google_calendar_id": account.google_calendar_id, "google_calendar_event_id": event.get("id")}, "name"),
"content": " - Event deleted from Google Calendar.",
}).insert(ignore_permissions=True)
else:
pass
if not results:
return _("No Google Calendar Event to sync.")
elif len(results) == 1:
return _("1 Google Calendar Event synced.")
else:
return _("{0} Google Calendar Events synced.").format(len(results))
def insert_event_to_calendar(account, event, recurrence=None):
"""
Inserts event in Frappe Calendar during Sync
"""
calendar_event = {
"doctype": "Event",
"subject": event.get("summary"),
"description": event.get("description"),
"google_calendar_event": 1,
"google_calendar": account.name,
"google_calendar_id": account.google_calendar_id,
"google_calendar_event_id": event.get("id"),
"pulled_from_google_calendar": 1
}
calendar_event.update(google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end")))
frappe.get_doc(calendar_event).insert(ignore_permissions=True)
def update_event_in_calendar(account, event, recurrence=None):
"""
Updates Event in Frappe Calendar if any existing Google Calendar Event is updated
"""
calendar_event = frappe.get_doc("Event", {"google_calendar_event_id": event.get("id")})
calendar_event.subject = event.get("summary")
calendar_event.description = event.get("description")
calendar_event.update(google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end")))
calendar_event.save(ignore_permissions=True)
def insert_event_in_google_calendar(doc, method=None):
"""
Insert Events in Google Calendar if sync_with_google_calendar is checked.
"""
if not frappe.db.exists("Google Calendar", {"name": doc.google_calendar}) or doc.pulled_from_google_calendar \
or not doc.sync_with_google_calendar:
return
google_calendar, account = get_google_calendar_object(doc.google_calendar)
if not account.push_to_google_calendar:
return
event = {
"summary": doc.subject,
"description": doc.description,
"google_calendar_event": 1
}
event.update(format_date_according_to_google_calendar(doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on)))
if doc.repeat_on:
event.update({"recurrence": repeat_on_to_google_calendar_recurrence_rule(doc)})
try:
event = google_calendar.events().insert(calendarId=doc.google_calendar_id, body=event).execute()
frappe.db.set_value("Event", doc.name, "google_calendar_event_id", event.get("id"), update_modified=False)
frappe.msgprint(_("Event Synced with Google Calendar."))
except HttpError as err:
frappe.throw(_("Google Calendar - Could not insert event in Google Calendar {0}, error code {1}.").format(account.name, err.resp.status))
def update_event_in_google_calendar(doc, method=None):
"""
Updates Events in Google Calendar if any existing event is modified in Frappe Calendar
"""
# Workaround to avoid triggering updation when Event is being inserted since
# creation and modified are same when inserting doc
if not frappe.db.exists("Google Calendar", {"name": doc.google_calendar}) or doc.modified == doc.creation \
or not doc.sync_with_google_calendar:
return
if doc.sync_with_google_calendar and not doc.google_calendar_event_id:
# If sync_with_google_calendar is checked later, then insert the event rather than updating it.
insert_event_in_google_calendar(doc)
return
google_calendar, account = get_google_calendar_object(doc.google_calendar)
if not account.push_to_google_calendar:
return
try:
event = google_calendar.events().get(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id).execute()
event["summary"] = doc.subject
event["description"] = doc.description
event["recurrence"] = repeat_on_to_google_calendar_recurrence_rule(doc)
event["status"] = "cancelled" if doc.event_type == "Cancelled" or doc.status == "Closed" else event.get("status")
event.update(format_date_according_to_google_calendar(doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on)))
google_calendar.events().update(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id, body=event).execute()
frappe.msgprint(_("Event Synced with Google Calendar."))
except HttpError as err:
frappe.throw(_("Google Calendar - Could not update Event {0} in Google Calendar, error code {1}.").format(doc.name, err.resp.status))
def delete_event_from_google_calendar(doc, method=None):
"""
Delete Events from Google Calendar if Frappe Event is deleted.
"""
if not frappe.db.exists("Google Calendar", {"name": doc.google_calendar}):
return
google_calendar, account = get_google_calendar_object(doc.google_calendar)
if not account.push_to_google_calendar:
return
try:
event = google_calendar.events().get(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id).execute()
event["recurrence"] = None
event["status"] = "cancelled"
google_calendar.events().update(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id, body=event).execute()
except HttpError as err:
frappe.msgprint(_("Google Calendar - Could not delete Event {0} from Google Calendar, error code {1}.").format(doc.name, err.resp.status))
def google_calendar_to_repeat_on(start, end, recurrence=None):
"""
recurrence is in the form ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH']
has the frequency and then the days on which the event recurs
Both have been mapped in a dict for easier mapping.
"""
repeat_on = {
"starts_on": get_datetime(start.get("date")) if start.get("date") else parser.parse(start.get("dateTime")).utcnow(),
"ends_on": get_datetime(end.get("date")) if end.get("date") else parser.parse(end.get("dateTime")).utcnow(),
"all_day": 1 if start.get("date") else 0,
"repeat_this_event": 1 if recurrence else 0,
"repeat_on": None,
"repeat_till": None,
"sunday": 0,
"monday": 0,
"tuesday": 0,
"wednesday": 0,
"thursday": 0,
"friday": 0,
"saturday": 0,
}
# recurrence rule "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH"
if recurrence:
# google_calendar_frequency = RRULE:FREQ=WEEKLY, byday = BYDAY=MO,TU,TH, until = 20191028
google_calendar_frequency, until, byday = get_recurrence_parameters(recurrence)
repeat_on["repeat_on"] = google_calendar_frequencies.get(google_calendar_frequency)
if repeat_on["repeat_on"] == "Daily":
repeat_on["ends_on"] = None
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None
if byday and repeat_on["repeat_on"] == "Weekly":
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None
byday = byday.split("=")[1].split(",")
for repeat_day in byday:
repeat_on[google_calendar_days[repeat_day]] = 1
if byday and repeat_on["repeat_on"] == "Monthly":
byday = byday.split("=")[1]
repeat_day_week_number, repeat_day_name = None, None
for num in ["-2", "-1", "1", "2", "3", "4", "5"]:
if num in byday:
repeat_day_week_number = num
break
for day in ["MO","TU","WE","TH","FR","SA","SU"]:
if day in byday:
repeat_day_name = google_calendar_days.get(day)
break
# Only Set starts_on for the event to repeat monthly
start_date = parse_google_calendar_recurrence_rule(int(repeat_day_week_number), repeat_day_name)
repeat_on["starts_on"] = start_date
repeat_on["ends_on"] = add_to_date(start_date, minutes=5)
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None
if repeat_on["repeat_till"] == "Yearly":
repeat_on["ends_on"] = None
repeat_on["repeat_till"] = datetime.strptime(until, "%Y%m%d") if until else None
return repeat_on
def format_date_according_to_google_calendar(all_day, starts_on, ends_on=None):
if not ends_on:
ends_on = starts_on + timedelta(minutes=10)
date_format = {
"start": {
"dateTime": starts_on.isoformat(),
"timeZone": get_time_zone(),
},
"end": {
"dateTime": ends_on.isoformat(),
"timeZone": get_time_zone(),
}
}
if all_day:
# If all_day event, Google Calendar takes date as a parameter and not dateTime
date_format["start"].pop("dateTime")
date_format["end"].pop("dateTime")
date_format["start"].update({"date": starts_on.date().isoformat()})
date_format["end"].update({"date": ends_on.date().isoformat()})
return date_format
def parse_google_calendar_recurrence_rule(repeat_day_week_number, repeat_day_name):
"""
Returns (repeat_on) exact date for combination eg 4TH viz. 4th thursday of a month
"""
if repeat_day_week_number < 0:
# Consider a month with 5 weeks and event is to be repeated in last week of every month, google caledar considers
# a month has 4 weeks and hence itll return -1 for a month with 5 weeks.
repeat_day_week_number = 4
weekdays = get_weekdays()
current_date = now_datetime()
isset_day_name, isset_day_number = False, False
# Set the proper day ie if recurrence is 4TH, then align the day to Thursday
while not isset_day_name:
isset_day_name = True if weekdays[current_date.weekday()].lower() == repeat_day_name else False
current_date = add_days(current_date, 1) if not isset_day_name else current_date
# One the day is set to Thursday, now set the week number ie 4
while not isset_day_number:
week_number = get_week_number(current_date)
isset_day_number = True if week_number == repeat_day_week_number else False
# check if current_date week number is greater or smaller than repeat_day week number
weeks = 1 if week_number < repeat_day_week_number else -1
current_date = add_to_date(current_date, weeks=weeks) if not isset_day_number else current_date
return current_date
def repeat_on_to_google_calendar_recurrence_rule(doc):
"""
Returns event (repeat_on) in Google Calendar format ie RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH
"""
recurrence = framework_frequencies.get(doc.repeat_on)
weekdays = get_weekdays()
if doc.repeat_on == "Weekly":
byday = [framework_days.get(day.lower()) for day in weekdays if doc.get(day.lower())]
recurrence = recurrence + "BYDAY=" + ",".join(byday)
elif doc.repeat_on == "Monthly":
week_number = str(get_week_number(get_datetime(doc.starts_on)))
week_day = weekdays[get_datetime(doc.starts_on).weekday()].lower()
recurrence = recurrence + "BYDAY=" + week_number + framework_days.get(week_day)
return [recurrence]
def get_week_number(dt):
"""
Returns the week number of the month for the specified date.
https://stackoverflow.com/questions/3806473/python-week-number-of-the-month/16804556
"""
from math import ceil
first_day = dt.replace(day=1)
dom = dt.day
adjusted_dom = dom + first_day.weekday()
return int(ceil(adjusted_dom/7.0))
def get_recurrence_parameters(recurrence):
recurrence = recurrence.split(";")
frequency, until, byday = None, None, None
for r in recurrence:
if "RRULE:FREQ" in r:
frequency = r
elif "UNTIL" in r:
until = r
elif "BYDAY" in r:
byday = r
else:
pass
return frequency, until, byday
"""API Response
{
'kind': 'calendar#events',
'etag': '"etag"',
'summary': 'Test Calendar',
'updated': '2019-07-25T06:09:34.681Z',
'timeZone': 'Asia/Kolkata',
'accessRole': 'owner',
'defaultReminders': [],
'nextSyncToken': 'token',
'items': [
{
'kind': 'calendar#event',
'etag': '"etag"',
'id': 'id',
'status': 'confirmed' or 'cancelled',
'htmlLink': 'link',
'created': '2019-07-25T06:08:21.000Z',
'updated': '2019-07-25T06:09:34.681Z',
'summary': 'asdf',
'creator': {
'email': 'email'
},
'organizer': {
'email': 'email',
'displayName': 'Test Calendar',
'self': True
},
'start': {
'dateTime': '2019-07-27T12:00:00+05:30', (if all day event the its 'date' instead of 'dateTime')
'timeZone': 'Asia/Kolkata'
},
'end': {
'dateTime': '2019-07-27T13:00:00+05:30', (if all day event the its 'date' instead of 'dateTime')
'timeZone': 'Asia/Kolkata'
},
'recurrence': *recurrence,
'iCalUID': 'uid',
'sequence': 1,
'reminders': {
'useDefault': True
}
}
]
}
*recurrence
- Daily Event: ['RRULE:FREQ=DAILY']
- Weekly Event: ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH']
- Monthly Event: ['RRULE:FREQ=MONTHLY;BYDAY=4TH']
- BYDAY: -2, -1, 1, 2, 3, 4 with weekdays (-2 edge case for April 2017 had 6 weeks in a month)
- Yearly Event: ['RRULE:FREQ=YEARLY;']
- Custom Event: ['RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20191028;BYDAY=MO,WE']"""

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestGoogleCalendar(unittest.TestCase):
pass

View file

@ -16,7 +16,7 @@ frappe.ui.form.on('Google Contacts', {
});
if (frm.doc.refresh_token) {
frm.add_custom_button(__('Sync Contacts'), function () {
let sync_button = frm.add_custom_button(__('Sync Contacts'), function () {
frappe.show_alert({
indicator: 'green',
message: __('Syncing')
@ -26,6 +26,7 @@ frappe.ui.form.on('Google Contacts', {
args: {
"g_contact": frm.doc.name
},
btn: sync_button
}).then((r) => {
frappe.hide_progress();
frappe.msgprint(r.message);

View file

@ -2,8 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Google Settings', {
enable: function(frm) {
frm.set_df_property('client_id', 'reqd', frm.doc.enable ? 1 : 0);
frm.set_df_property('client_secret', 'reqd', frm.doc.enable ? 1 : 0);
}
// refresh: function(frm) {
// }
});

View file

@ -4,9 +4,10 @@
"engine": "InnoDB",
"field_order": [
"enable",
"google_credentials",
"sb_00",
"client_id",
"client_secret",
"sb_01",
"api_key"
],
"fields": [
@ -16,12 +17,6 @@
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "google_credentials",
"fieldtype": "Section Break",
"label": "Google Credentials"
},
{
"fieldname": "client_id",
"fieldtype": "Data",
@ -35,13 +30,26 @@
"label": "Client Secret"
},
{
"description": "Used For Google Maps Integration.",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
},
{
"depends_on": "enable",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "OAuth Client ID"
},
{
"depends_on": "enable",
"fieldname": "sb_01",
"fieldtype": "Section Break",
"label": "API Key"
}
],
"issingle": 1,
"modified": "2019-06-29 13:26:33.201060",
"modified": "2019-08-06 22:37:41.699703",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",

View file

@ -580,29 +580,28 @@ class DatabaseQuery(object):
meta = frappe.get_meta(self.doctype)
doctype_link_fields = []
doctype_link_fields = meta.get_link_fields()
# append current doctype with fieldname as 'name' as first link field
doctype_link_fields.append(dict(
options=self.doctype,
fieldname='name',
))
# appended current doctype with fieldname as 'name' to
# and condition on doc name if user permission is found for current doctype
match_filters = {}
match_conditions = []
for df in doctype_link_fields:
user_permission_values = user_permissions.get(df.get('options'), {})
if df.get('ignore_user_permissions'): continue
empty_value_condition = "ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(
doctype=self.doctype, fieldname=df.get('fieldname')
)
user_permission_values = user_permissions.get(df.get('options'), {})
if user_permission_values:
docs = []
if frappe.get_system_settings("apply_strict_user_permissions"):
condition = ""
else:
empty_value_condition = "ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(
doctype=self.doctype, fieldname=df.get('fieldname')
)
condition = empty_value_condition + " or "
for permission in user_permission_values:
@ -611,9 +610,10 @@ class DatabaseQuery(object):
# append docs based on user permission applicable on reference doctype
# This is useful when getting list of doc from a link field
# in this case parent doctype of the link will be the
# will be the reference doctype
# this is useful when getting list of docs from a link field
# in this case parent doctype of the link
# will be the reference doctype
elif df.get('fieldname') == 'name' and self.reference_doctype:
if permission.get('applicable_for') == self.reference_doctype:

View file

@ -1,659 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
"""
Syncs a database table to the `DocType` (metadata)
.. note:: This module is only used internally
"""
import re
import os
import frappe
from frappe import _
from frappe.utils import cstr, cint, flt
# imports - third-party imports
import pymysql
from pymysql.constants import ER
class InvalidColumnName(frappe.ValidationError): pass
varchar_len = '140'
standard_varchar_columns = ('name', 'owner', 'modified_by', 'parent', 'parentfield', 'parenttype')
type_map = {
'Currency': ('decimal', '18,6'),
'Int': ('int', '11'),
'Long Int': ('bigint', '20'), # convert int to bigint if length is more than 11
'Float': ('decimal', '18,6'),
'Percent': ('decimal', '18,6'),
'Check': ('int', '1'),
'Small Text': ('text', ''),
'Long Text': ('longtext', ''),
'Code': ('longtext', ''),
'Text Editor': ('longtext', ''),
'HTML Editor': ('longtext', ''),
'Markdown Editor': ('longtext', ''),
'Date': ('date', ''),
'Datetime': ('datetime', '6'),
'Time': ('time', '6'),
'Text': ('text', ''),
'Data': ('varchar', varchar_len),
'Link': ('varchar', varchar_len),
'Dynamic Link': ('varchar', varchar_len),
'Password': ('varchar', varchar_len),
'Select': ('varchar', varchar_len),
'Rating': ('int', '1'),
'Read Only': ('varchar', varchar_len),
'Attach': ('text', ''),
'Attach Image': ('text', ''),
'Signature': ('longtext', ''),
'Color': ('varchar', varchar_len),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', '')
}
default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner',
'docstatus', 'parent', 'parentfield', 'parenttype', 'idx']
optional_columns = ["_user_tags", "_comments", "_assign", "_liked_by"]
default_shortcuts = ['_Login', '__user', '_Full Name', 'Today', '__today', "now", "Now"]
def updatedb(dt, meta=None):
"""
Syncs a `DocType` to the table
* creates if required
* updates columns
* updates indices
"""
res = frappe.db.sql("select issingle from tabDocType where name=%s", (dt,))
if not res:
raise Exception('Wrong doctype "%s" in updatedb' % dt)
if not res[0][0]:
tab = DbTable(dt, 'tab', meta)
tab.validate()
frappe.db.commit()
tab.sync()
frappe.db.begin()
class DbTable:
def __init__(self, doctype, prefix = 'tab', meta = None):
self.doctype = doctype
self.name = prefix + doctype
self.columns = {}
self.current_columns = {}
self.meta = meta
if not self.meta:
self.meta = frappe.get_meta(self.doctype)
# lists for change
self.add_column = []
self.change_type = []
self.add_index = []
self.drop_index = []
self.set_default = []
# load
self.get_columns_from_docfields()
def validate(self):
"""Check if change in varchar length isn't truncating the columns"""
if self.is_new():
return
self.get_columns_from_db()
columns = [frappe._dict({"fieldname": f, "fieldtype": "Data"}) for f in standard_varchar_columns]
columns += self.columns.values()
for col in columns:
if len(col.fieldname) >= 64:
frappe.throw(_("Fieldname is limited to 64 characters ({0})")
.format(frappe.bold(col.fieldname)))
if col.fieldtype in type_map and type_map[col.fieldtype][0]=="varchar":
# validate length range
new_length = cint(col.length) or cint(varchar_len)
if not (1 <= new_length <= 1000):
frappe.throw(_("Length of {0} should be between 1 and 1000").format(col.fieldname))
current_col = self.current_columns.get(col.fieldname, {})
if not current_col:
continue
current_type = self.current_columns[col.fieldname]["type"]
current_length = re.findall('varchar\(([\d]+)\)', current_type)
if not current_length:
# case when the field is no longer a varchar
continue
current_length = current_length[0]
if cint(current_length) != cint(new_length):
try:
# check for truncation
max_length = frappe.db.sql("""select max(char_length(`{fieldname}`)) from `tab{doctype}`"""\
.format(fieldname=col.fieldname, doctype=self.doctype))
except pymysql.InternalError as e:
if e.args[0] == ER.BAD_FIELD_ERROR:
# Unknown column 'column_name' in 'field list'
continue
else:
raise
if max_length and max_length[0][0] and max_length[0][0] > new_length:
if col.fieldname in self.columns:
self.columns[col.fieldname].length = current_length
frappe.msgprint(_("Reverting length to {0} for '{1}' in '{2}'; Setting the length as {3} will cause truncation of data.")\
.format(current_length, col.fieldname, self.doctype, new_length))
def sync(self):
if self.is_new():
self.create()
else:
self.alter()
def is_new(self):
return self.name not in DbManager(frappe.db).get_tables_list(frappe.db.cur_db_name)
def create(self):
add_text = ''
# columns
column_defs = self.get_column_definitions()
if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
# index
index_defs = self.get_index_definitions()
if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
# create table
frappe.db.sql("""create table `%s` (
name varchar({varchar_len}) not null primary key,
creation datetime(6),
modified datetime(6),
modified_by varchar({varchar_len}),
owner varchar({varchar_len}),
docstatus int(1) not null default '0',
parent varchar({varchar_len}),
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx int(8) not null default '0',
%sindex parent(parent),
index modified(modified))
ENGINE={engine}
ROW_FORMAT=COMPRESSED
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_unicode_ci""".format(varchar_len=varchar_len,
engine=self.meta.get("engine") or 'InnoDB') % (self.name, add_text))
def get_column_definitions(self):
column_list = [] + default_columns
ret = []
for k in list(self.columns):
if k not in column_list:
d = self.columns[k].get_definition()
if d:
ret.append('`'+ k + '` ' + d)
column_list.append(k)
return ret
def get_index_definitions(self):
ret = []
for key, col in self.columns.items():
if col.set_index and not col.unique and col.fieldtype in type_map and \
type_map.get(col.fieldtype)[0] not in ('text', 'longtext'):
ret.append('index `' + key + '`(`' + key + '`)')
return ret
def get_columns_from_docfields(self):
"""
get columns from docfields and custom fields
"""
fl = frappe.db.sql("SELECT * FROM tabDocField WHERE parent = %s", self.doctype, as_dict = 1)
lengths = {}
precisions = {}
uniques = {}
# optional fields like _comments
if not self.meta.istable:
for fieldname in optional_columns:
fl.append({
"fieldname": fieldname,
"fieldtype": "Text"
})
# add _seen column if track_seen
if getattr(self.meta, 'track_seen', False):
fl.append({
'fieldname': '_seen',
'fieldtype': 'Text'
})
if not frappe.flags.in_install_db and (frappe.flags.in_install != "frappe" or frappe.flags.ignore_in_install):
custom_fl = frappe.db.sql("""\
SELECT * FROM `tabCustom Field`
WHERE dt = %s AND docstatus < 2""", (self.doctype,), as_dict=1)
if custom_fl: fl += custom_fl
# apply length, precision and unique from property setters
for ps in frappe.get_all("Property Setter", fields=["field_name", "property", "value"],
filters={
"doc_type": self.doctype,
"doctype_or_field": "DocField",
"property": ["in", ["precision", "length", "unique"]]
}):
if ps.property=="length":
lengths[ps.field_name] = cint(ps.value)
elif ps.property=="precision":
precisions[ps.field_name] = cint(ps.value)
elif ps.property=="unique":
uniques[ps.field_name] = cint(ps.value)
for f in fl:
self.columns[f['fieldname']] = DbColumn(self, f['fieldname'],
f['fieldtype'], lengths.get(f["fieldname"]) or f.get('length'), f.get('default'), f.get('search_index'),
f.get('options'), uniques.get(f["fieldname"], f.get('unique')), precisions.get(f['fieldname']) or f.get('precision'))
def get_columns_from_db(self):
self.show_columns = frappe.db.sql("desc `%s`" % self.name)
for c in self.show_columns:
self.current_columns[c[0].lower()] = {'name': c[0],
'type':c[1], 'index':c[3]=="MUL", 'default':c[4], "unique":c[3]=="UNI"}
# GET foreign keys
def get_foreign_keys(self):
fk_list = []
txt = frappe.db.sql("show create table `%s`" % self.name)[0][1]
for line in txt.split('\n'):
if line.strip().startswith('CONSTRAINT') and line.find('FOREIGN')!=-1:
try:
fk_list.append((line.split('`')[3], line.split('`')[1]))
except IndexError:
pass
return fk_list
# Drop foreign keys
def drop_foreign_keys(self):
if not self.drop_foreign_key:
return
fk_list = self.get_foreign_keys()
# make dictionary of constraint names
fk_dict = {}
for f in fk_list:
fk_dict[f[0]] = f[1]
# drop
for col in self.drop_foreign_key:
frappe.db.sql("set foreign_key_checks=0")
frappe.db.sql("alter table `%s` drop foreign key `%s`" % (self.name, fk_dict[col.fieldname]))
frappe.db.sql("set foreign_key_checks=1")
def alter(self):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower(), None))
query = []
for col in self.add_column:
query.append("add column `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.change_type:
current_def = self.current_columns.get(col.fieldname.lower(), None)
query.append("change `{}` `{}` {}".format(current_def["name"], col.fieldname, col.get_definition()))
for col in self.add_index:
# if index key not exists
if not frappe.db.sql("show index from `%s` where key_name = %s" %
(self.name, '%s'), col.fieldname):
query.append("add index `{}`(`{}`)".format(col.fieldname, col.fieldname))
for col in self.drop_index:
if col.fieldname != 'name': # primary key
# if index key exists
if frappe.db.sql("""show index from `{0}`
where key_name=%s
and Non_unique=%s""".format(self.name), (col.fieldname, col.unique)):
query.append("drop index `{}`".format(col.fieldname))
for col in self.set_default:
if col.fieldname=="name":
continue
if col.fieldtype in ("Check", "Int"):
col_default = cint(col.default)
elif col.fieldtype in ("Currency", "Float", "Percent"):
col_default = flt(col.default)
elif not col.default:
col_default = "null"
else:
col_default = '"{}"'.format(col.default.replace('"', '\\"'))
query.append('alter column `{}` set default {}'.format(col.fieldname, col_default))
if query:
try:
frappe.db.sql("alter table `{}` {}".format(self.name, ", ".join(query)))
except Exception as e:
# sanitize
if e.args[0]==1060:
frappe.throw(str(e))
elif e.args[0]==1062:
fieldname = str(e).split("'")[-2]
frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values".format(fieldname, self.name)))
else:
raise e
class DbColumn:
def __init__(self, table, fieldname, fieldtype, length, default,
set_index, options, unique, precision):
self.table = table
self.fieldname = fieldname
self.fieldtype = fieldtype
self.length = length
self.set_index = set_index
self.default = default
self.options = options
self.unique = unique
self.precision = precision
def get_definition(self, with_default=1):
column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
if not column_def:
return column_def
if self.fieldtype in ("Check", "Int"):
default_value = cint(self.default) or 0
column_def += ' not null default {0}'.format(default_value)
elif self.fieldtype in ("Currency", "Float", "Percent"):
default_value = flt(self.default) or 0
column_def += ' not null default {0}'.format(default_value)
elif self.default and (self.default not in default_shortcuts) \
and not self.default.startswith(":") and column_def not in ('text', 'longtext'):
column_def += ' default "' + self.default.replace('"', '\"') + '"'
if self.unique and (column_def not in ('text', 'longtext')):
column_def += ' unique'
return column_def
def build_for_alter_table(self, current_def):
column_def = get_definition(self.fieldtype, self.precision, self.length)
# no columns
if not column_def:
return
# to add?
if not current_def:
self.fieldname = validate_column_name(self.fieldname)
self.table.add_column.append(self)
return
# type
if (current_def['type'] != column_def) or\
self.fieldname != current_def['name'] or\
((self.unique and not current_def['unique']) and column_def not in ('text', 'longtext')):
self.table.change_type.append(self)
else:
# default
if (self.default_changed(current_def) \
and (self.default not in default_shortcuts) \
and not cstr(self.default).startswith(":") \
and not (column_def in ['text','longtext'])):
self.table.set_default.append(self)
# index should be applied or dropped irrespective of type change
if ( (current_def['index'] and not self.set_index and not self.unique)
or (current_def['unique'] and not self.unique) ):
# to drop unique you have to drop index
self.table.drop_index.append(self)
elif (not current_def['index'] and self.set_index) and not (column_def in ('text', 'longtext')):
self.table.add_index.append(self)
def default_changed(self, current_def):
if "decimal" in current_def['type']:
return self.default_changed_for_decimal(current_def)
else:
return current_def['default'] != self.default
def default_changed_for_decimal(self, current_def):
try:
if current_def['default'] in ("", None) and self.default in ("", None):
# both none, empty
return False
elif current_def['default'] in ("", None):
try:
# check if new default value is valid
float(self.default)
return True
except ValueError:
return False
elif self.default in ("", None):
# new default value is empty
return True
else:
# NOTE float() raise ValueError when "" or None is passed
return float(current_def['default'])!=float(self.default)
except TypeError:
return True
class DbManager:
"""
Basically, a wrapper for oft-used mysql commands. like show tables,databases, variables etc...
#TODO:
0. Simplify / create settings for the restore database source folder
0a. Merge restore database and extract_sql(from frappe_server_tools).
1. Setter and getter for different mysql variables.
2. Setter and getter for mysql variables at global level??
"""
def __init__(self,db):
"""
Pass root_conn here for access to all databases.
"""
if db:
self.db = db
def get_current_host(self):
return self.db.sql("select user()")[0][0].split('@')[1]
def get_variables(self,regex):
"""
Get variables that match the passed pattern regex
"""
return list(self.db.sql("SHOW VARIABLES LIKE '%s'"%regex))
def get_table_schema(self,table):
"""
Just returns the output of Desc tables.
"""
return list(self.db.sql("DESC `%s`"%table))
def get_tables_list(self,target=None):
"""get list of tables"""
if target:
self.db.use(target)
return [t[0] for t in self.db.sql("SHOW TABLES")]
def create_user(self, user, password, host=None):
#Create user if it doesn't exist.
if not host:
host = self.get_current_host()
if password:
self.db.sql("CREATE USER '%s'@'%s' IDENTIFIED BY '%s';" % (user[:16], host, password))
else:
self.db.sql("CREATE USER '%s'@'%s';" % (user[:16], host))
def delete_user(self, target, host=None):
if not host:
host = self.get_current_host()
try:
self.db.sql("DROP USER '%s'@'%s';" % (target, host))
except Exception as e:
if e.args[0]==1396:
pass
else:
raise
def create_database(self,target):
if target in self.get_database_list():
self.drop_database(target)
self.db.sql("CREATE DATABASE `%s` ;" % target)
def drop_database(self,target):
self.db.sql("DROP DATABASE IF EXISTS `%s`;"%target)
def grant_all_privileges(self, target, user, host=None):
if not host:
host = self.get_current_host()
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target,
user, host))
def grant_select_privilges(self, db, table, user, host=None):
if not host:
host = self.get_current_host()
if table:
self.db.sql("GRANT SELECT ON %s.%s to '%s'@'%s';" % (db, table, user, host))
else:
self.db.sql("GRANT SELECT ON %s.* to '%s'@'%s';" % (db, user, host))
def flush_privileges(self):
self.db.sql("FLUSH PRIVILEGES")
def get_database_list(self):
"""get list of databases"""
return [d[0] for d in self.db.sql("SHOW DATABASES")]
def restore_database(self,target,source,user,password):
from frappe.utils import make_esc
esc = make_esc('$ ')
from distutils.spawn import find_executable
pipe = find_executable('pv')
if pipe:
pipe = '{pipe} {source} |'.format(
pipe = pipe,
source = source
)
source = ''
else:
pipe = ''
source = '< {source}'.format(source = source)
if pipe:
print('Creating Database...')
command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format(
pipe = pipe,
user = esc(user),
password = esc(password),
host = esc(frappe.db.host),
target = esc(target),
source = source
)
os.system(command)
def drop_table(self,table_name):
"""drop table if exists"""
if not table_name in self.get_tables_list():
return
self.db.sql("DROP TABLE IF EXISTS %s "%(table_name))
def validate_column_name(n):
special_characters = re.findall("[\W]", n, re.UNICODE)
if special_characters:
special_characters = ", ".join('"{0}"'.format(c) for c in special_characters)
frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(frappe.bold(cstr(n)), special_characters), InvalidColumnName)
return n
def validate_column_length(fieldname):
""" In MySQL maximum column length is 64 characters,
ref: https://dev.mysql.com/doc/refman/5.5/en/identifiers.html"""
if len(fieldname) > 64:
frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname))
def remove_all_foreign_keys():
frappe.db.sql("set foreign_key_checks = 0")
frappe.db.commit()
for t in frappe.db.sql("select name from tabDocType where issingle=0"):
dbtab = DbTable(t[0])
try:
fklist = dbtab.get_foreign_keys()
except Exception as e:
if e.args[0]==1146:
fklist = []
else:
raise
for f in fklist:
frappe.db.sql("alter table `tab%s` drop foreign key `%s`" % (t[0], f[1]))
def get_definition(fieldtype, precision=None, length=None):
d = type_map.get(fieldtype)
# convert int to long int if the length of the int is greater than 11
if fieldtype == "Int" and length and length>11:
d = type_map.get("Long Int")
if not d:
return
coltype = d[0]
size = None
if d[1]:
size = d[1]
if size:
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = '21,9'
if coltype == "varchar" and length:
size = length
if size is not None:
coltype = "{coltype}({size})".format(coltype=coltype, size=size)
return coltype
def add_column(doctype, column_name, fieldtype, precision=None):
if column_name in frappe.db.get_table_columns(doctype):
# already exists
return
frappe.db.commit()
frappe.db.sql("alter table `tab%s` add column %s %s" % (doctype,
column_name, get_definition(fieldtype, precision)))

View file

@ -20,15 +20,15 @@ def execute():
if communication.timeline_doctype and communication.timeline_name:
name += 1
values.append("""({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format(
counter, str(name), communication.name, communication.timeline_doctype,
communication.timeline_name, communication.creation, communication.modified, communication.modified_by
counter, str(name), frappe.db.escape(communication.name), frappe.db.escape(communication.timeline_doctype),
frappe.db.escape(communication.timeline_name), communication.creation, communication.modified, communication.modified_by
))
counter += 1
if communication.link_doctype and communication.link_name:
name += 1
values.append("""({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format(
counter, str(name), communication.name, communication.link_doctype,
communication.link_name, communication.creation, communication.modified, communication.modified_by
counter, str(name), frappe.db.escape(communication.name), frappe.db.escape(communication.link_doctype),
frappe.db.escape(communication.link_name), communication.creation, communication.modified, communication.modified_by
))
if values and (count % 10000 == 0 or count == len(communications) - 1):
@ -41,4 +41,4 @@ def execute():
values = []
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])

View file

@ -0,0 +1,10 @@
import frappe
def execute():
'''
Remove GCalendar and GCalendar Settings
Remove Google Maps Settings as its been merged with Delivery Trips
'''
frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')

View file

@ -648,6 +648,13 @@ frappe.PrintFormatBuilder = Class.extend({
d.hide();
});
let update_column_count_message = () => {
// show a warning if user selects more than 10 columns for a table
let columns_count = $body.find("input:checked").length;
$body.find('.help-message').toggle(columns_count > 10);
}
update_column_count_message();
// enable / disable input based on selection
$body.on("click", "input[type='checkbox']", function() {
var disabled = !$(this).prop("checked"),
@ -655,6 +662,8 @@ frappe.PrintFormatBuilder = Class.extend({
input.prop("disabled", disabled);
if(disabled) input.val("");
update_column_count_message();
});
d.show();

View file

@ -1,5 +1,8 @@
<p class="text-muted">{{ __("Check columns to select, drag to set order.") }}
<br>{{ __("Widths can be set in px or %.") }}</p>
{{ __("Widths can be set in px or %.") }}</p>
<p class="help-message alert alert-warning">
{{ __("Some columns might get cut off when printing to PDF. Try to keep number of columns under 10.") }}
</p>
<div class="row">
<div class="col-sm-6"><h4>{{ __("Column") }}</h4></div>
<div class="col-sm-6 text-right"><h4>{{ __("Width") }}</h4></div>

View file

@ -68,7 +68,7 @@ frappe.ui.form.Control = Class.extend({
// hide if no value
if (this.doctype && status==="Read" && !this.only_input
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
&& !in_list(["HTML", "Image"], this.df.fieldtype)) {
&& !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) {
// eslint-disable-next-line
if(explain) console.log("By Hide Read-only, null fields: None");

View file

@ -56,16 +56,16 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
refresh_input: function() {
var me = this;
var make_input = function() {
if(!me.has_input) {
if (!me.has_input) {
me.make_input();
if(me.df.on_make) {
if (me.df.on_make) {
me.df.on_make(me);
}
}
};
var update_input = function() {
if(me.doctype && me.docname) {
if (me.doctype && me.docname) {
me.set_input(me.value);
} else {
me.set_input(me.value || null);
@ -74,21 +74,20 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
if (me.disp_status != "None") {
// refresh value
if(me.frm) {
if (me.frm) {
me.value = frappe.model.get_value(me.doctype, me.docname, me.df.fieldname);
}
else if(me.doc) {
} else if (me.doc) {
me.value = me.doc[me.df.fieldname];
}
if(me.disp_status=="Write") {
if (me.can_write()) {
me.disp_area && $(me.disp_area).toggle(false);
$(me.input_area).toggle(true);
me.$input && me.$input.prop("disabled", false);
make_input();
update_input();
} else {
if(me.only_input) {
if (me.only_input) {
make_input();
update_input();
} else {
@ -108,6 +107,10 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
}
},
can_write() {
return this.disp_status == "Write";
},
set_disp_area: function(value) {
if(in_list(["Currency", "Int", "Float"], this.df.fieldtype)
&& (this.value === 0 || value === 0)) {

View file

@ -1,4 +1,8 @@
frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
can_write() {
// should be always true in case of button
return true;
},
make_input: function() {
var me = this;
this.$input = $('<button class="btn btn-default btn-xs">')

View file

@ -184,7 +184,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
}
}
if(!me.df.only_select && me.frm) {
if(!me.df.only_select) {
if(frappe.model.can_create(doctype)) {
// new item
r.results.push({

View file

@ -644,8 +644,8 @@ frappe.ui.form.Form = class FrappeForm {
}
amend_doc() {
if(!this.fields_dict['amended_from']) {
alert('"amended_from" field must be present to do an amendment.');
if (!this.fields_dict['amended_from']) {
frappe.msgprint(__('"amended_from" field must be present to do an amendment.'));
return;
}
this.validate_form_action("Amend");

View file

@ -6,8 +6,9 @@
<span :class="['indicator', indicator_color]"></span>
<span v-if="disabled_dependent" class="link-content text-muted">{{ label || __(name) }}</span>
<a v-else class="link-content" :href="route">{{ label || __(name) }}</a>
<a v-else class="link-content" :href="route" @click.prevent="handle_click">
{{ label || __(name) }}
</a>
<div v-if="disabled_dependent" v-show="popover_active"
@mouseover="popover_hover = true" @mouseleave="popover_hover = false"
class="module-link-popover popover fade top in" role="tooltip"
@ -24,7 +25,8 @@
<script>
export default {
props: ['label', 'name', 'dependencies', 'incomplete_dependencies', 'onboard', 'count', 'route', 'doctype', 'open_count'],
props: ['label', 'name', 'dependencies', 'incomplete_dependencies',
'onboard', 'count', 'route', 'doctype', 'open_count', 'youtube_id'],
data() {
return {
hover: false,
@ -60,6 +62,14 @@ export default {
setTimeout(() => {
this.hover = false;
}, 300);
},
handle_click(e) {
if (this.youtube_id) {
frappe.help.show_video(this.youtube_id);
} else {
frappe.set_route(this.route);
}
}
}
}

View file

@ -50,7 +50,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Reference Doctype",
"label": "Reference Document Type",
"options": "DocType",
"read_only": 1,
"search_index": 1
@ -101,7 +101,7 @@
}
],
"in_create": 1,
"modified": "2019-06-05 11:18:45.286367",
"modified": "2019-08-07 15:51:05.288886",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Log",

View file

@ -20,10 +20,10 @@
</div>
{% endif %}
{% for section in page %}
{% for section in page %}
<div class="row section-break">
{%- if doc._line_breaks and loop.index != 1 -%}<hr>{%- endif -%}
{%- if doc._show_section_headings and section.label and section.has_data -%}
{%- if doc.print_line_breaks and loop.index != 1 -%}<hr>{%- endif -%}
{%- if doc.print_section_headings and section.label and section.has_data -%}
<h4 class='col-sm-12'>{{ _(section.label) }}</h4>
{%- endif -%}
{% for column in section.columns %}

View file

@ -71,7 +71,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{%- macro render_field_with_label(df, doc) -%}
<div class="row {% if df.bold %}important{% endif %} data-field" {{ fieldmeta(df) }}>
<div class="col-xs-{{ "9" if df.fieldtype=="Check" else "5" }}
{%- if doc._align_labels_right %} text-right{%- endif -%}">
{%- if doc.align_labels_right %} text-right{%- endif -%}">
{% if df.fieldtype not in ("Image","HTML","Check") and
doc.get(df.fieldname) != None %}

View file

@ -166,3 +166,6 @@ table td div {
max-height: 150px;
}
.print-format [data-fieldtype="Table"] {
overflow: auto;
}

View file

@ -12,7 +12,7 @@ base_template_path = "templates/www/printview.html"
standard_format = "templates/print_formats/standard.html"
@frappe.whitelist()
def download_multi_pdf(doctype, name, format=None):
def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
"""
Concatenate multiple docs as PDF .
@ -60,13 +60,13 @@ def download_multi_pdf(doctype, name, format=None):
# Concatenating pdf files
for i, ss in enumerate(result):
output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output)
output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
frappe.local.response.filename = "{doctype}.pdf".format(doctype=doctype.replace(" ", "-").replace("/", "-"))
else:
for doctype_name in doctype:
for doc_name in doctype[doctype_name]:
try:
output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output)
output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
except Exception:
frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name))
frappe.local.response.filename = "{}.pdf".format(name)

View file

@ -95,9 +95,9 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
# determine template
if print_format:
doc._show_section_headings = print_format.show_section_headings
doc._line_breaks = print_format.line_breaks
doc._align_labels_right = print_format.align_labels_right
doc.print_section_headings = print_format.show_section_headings
doc.print_line_breaks = print_format.line_breaks
doc.align_labels_right = print_format.align_labels_right
def get_template_from_string():
return jenv.from_string(get_print_format(doc.doctype,