Merge branch 'version-12-hotfix' into merge_v12_hotfix_1

This commit is contained in:
Sahil Khan 2019-08-20 16:09:39 +05:30
commit 9b65a7c4a1
67 changed files with 1074 additions and 2168 deletions

View file

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

View file

@ -0,0 +1,29 @@
# Version 12 Release Notes
### UI/UX Enhancements
1. [New Desktop](https://erpnext.com/docs/user/manual/en/using-erpnext/desktop)
1. [Keyboard Navigation](https://erpnext.com/docs/user/manual/en/using-erpnext/articles/keyboard-shortcuts)
1. [Link Preview](https://erpnext.com/version-12/release-notes/features#link-preview)
1. [New Upload Dialog](https://erpnext.com/version-12/release-notes/features#new-upload-dialog)
1. [Frequently visited links appear in Awesomebar results](https://erpnext.com/version-12/release-notes/features#frequently-visited-links-appear-in-awesomebar-results)
1. [Full Width Container]((https://erpnext.com/version-12/release-notes/features#full-width-container))
1. [List View Enhancements](https://erpnext.com/version-12/release-notes/features#list-view-enhancements)
### New Automation Module
1. [Assignment Rule](https://erpnext.com/docs/user/manual/en/setting-up/automation/assignment-rule)
1. [Milestones](https://erpnext.com/docs/user/manual/en/setting-up/automation/milestone-tracker)
1. [Auto Repeat](https://erpnext.com/docs/user/manual/en/setting-up/automation/auto-repeat)
### Other Changes & Enhancements
1. [Document Follow](https://erpnext.com/docs/user/manual/en/setting-up/email/document-follow)
1. [Energy Points](https://erpnext.com/docs/user/manual/en/setting-up/energy-point-system)
1. [Dashboards](https://erpnext.com/docs/user/manual/en/customize-erpnext/dashboard)
1. [Disable customization for single doctypes](https://erpnext.com/version-12/release-notes/features#disable-customization-for-single-doctypes)
1. [Email Linking](https://erpnext.com/docs/user/manual/en/setting-up/email/linking-emails-to-document)
1. [Google Contacts](https://erpnext.com/docs/user/manual/en/erpnext_integration/google_contacts)
1. [PDF Encryption](https://erpnext.com/version-12/release-notes/features#pdf-encryption)
1. [Raw Printing](https://erpnext.com/docs/user/manual/en/setting-up/print/raw-printing)
1. [Web Form Refactor](https://erpnext.com/version-12/release-notes/features#web-form-refactor)
1. [Website Refactor](https://erpnext.com/docs/user/manual/en/website)
1. [Added Track Views field to Customize Form](https://erpnext.com/version-12/release-notes/features#added-track-views-field-to-customize-form)
1. [Add custom columns to any report](https://erpnext.com/version-12/release-notes/features#add-custom-columns-to-any-report)

View file

@ -72,12 +72,10 @@ class ChatRoom(Document):
def on_update(self):
if not self.is_new():
before = self.get_doc_before_save()
if not before: return
after = self
diff = None
if before:
diff = dictify(get_diff(before, after))
diff = dictify(get_diff(before, after))
if diff:
update = { }
for changed in diff.changed:

View file

@ -29,13 +29,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"""
@ -66,7 +66,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

@ -15,7 +15,7 @@ from frappe.utils.background_jobs import enqueue
class DataImport(Document):
def autoname(self):
if not self.name:
self.name = "Import on "+ format_datetime(self.creation)
self.name = "Import on " +format_datetime(self.creation)
def validate(self):
if not self.import_file:

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

@ -120,12 +120,19 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
return has_same_user_permission
def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters):
linked_doctypes = get_linked_doctypes(doctype, True).keys()
linked_doctypes = list(linked_doctypes)
linked_doctypes_map = get_linked_doctypes(doctype, True)
linked_doctypes = []
for linked_doctype, linked_doctype_values in linked_doctypes_map.items():
linked_doctypes.append(linked_doctype)
child_doctype = linked_doctype_values.get("child_doctype")
if child_doctype:
linked_doctypes.append(child_doctype)
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

@ -3,9 +3,9 @@
<table class="table table-bordered" style="table-layout: fixed;">
<thead>
<tr>
<th style="width: 20%">Queue / Worker</th>
<th>Job</th>
<th style="width: 15%">Created</th>
<th style="width: 20%">{{ _("Queue / Worker") }}</th>
<th>{{ _("Job") }}</th>
<th style="width: 15%">{{ _("Created") }}</th>
</tr>
</thead>
<tbody>
@ -28,13 +28,13 @@
</tbody>
</table>
<p>
<span class="indicator blue" style="margin-right: 20px;">Started</span>
<span class="indicator orange" style="margin-right: 20px;">Queued</span>
<span class="indicator red" style="margin-right: 20px;">Failed</span>
<span class="indicator green">Finished</span>
<span class="indicator blue" style="margin-right: 20px;">{{ _("Started") }}</span>
<span class="indicator orange" style="margin-right: 20px;">{{ _("Queued") }}</span>
<span class="indicator red" style="margin-right: 20px;">{{ _("Failed") }}</span>
<span class="indicator green">{{ _("Finished") }}</span>
</p>
{% else %}
<p class="text-muted">No pending or current jobs for this site</p>
<p class="text-muted">{{ _("No pending or current jobs for this site") }}</p>
{% endif %}
<p class="text-muted" style="margin-top: 30px;">Last refreshed {{ frappe.datetime.now_datetime() }}</p>
</div>
<p class="text-muted" style="margin-top: 30px;">{{ _("Last refreshed") }} {{ frappe.datetime.now_datetime() }}</p>
</div>

View file

@ -1,7 +1,7 @@
frappe.pages['background_jobs'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Background Jobs',
title: __('Background Jobs'),
single_column: true
});

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

@ -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

@ -36,6 +36,18 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger('update_options');
},
timespan: function(frm) {
const time_interval_options = {
"Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"],
"Last Quarter": ["Monthly", "Weekly", "Daily"],
"Last Month": ["Weekly", "Daily"],
"Last Week": ["Daily"]
};
if (frm.doc.timespan) {
frm.set_df_property('time_interval', 'options', time_interval_options[frm.doc.timespan]);
}
},
update_options: function(frm) {
let doctype = frm.doc.document_type;
let date_fields = [

View file

@ -74,7 +74,6 @@ def get_aggregate_function(chart_type):
"Average": "AVG"
}[chart_type]
def convert_to_dates(data, timegrain):
result = []
for d in data:

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 12:28:43.144932",
"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

@ -91,7 +91,7 @@ def get_cached_contacts(txt):
if not txt:
return contacts
match = [d for d in contacts if (d.value and (d.value and txt in d.value or d.description and txt in d.description))]
match = [d for d in contacts if (d.value and ((d.value and txt in d.value) or (d.description and txt in d.description)))]
return match
def update_contact_cache(contacts):

View file

@ -481,7 +481,10 @@ class Email:
"""Detect chartset."""
charset = part.get_content_charset()
if not charset:
charset = chardet.detect(str(part))['encoding']
if six.PY2:
charset = chardet.detect(str(part))['encoding']
else:
charset = chardet.detect(part.encode())['encoding']
return charset

View file

@ -147,7 +147,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'
],
@ -158,6 +157,7 @@ scheduler_events = {
"frappe.desk.page.backups.backups.delete_downloadable_backups",
"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,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

@ -11,6 +11,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, split_emails
from frappe.utils.background_jobs import enqueue
from rq.timeouts import JobTimeoutException
from botocore.exceptions import ClientError
class S3BackupSettings(Document):

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

@ -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

@ -3494,7 +3494,7 @@ tbody.collapse.in {
right: 0;
bottom: 0;
left: 0;
z-index: 990;
z-index: 99;
}
.pull-right > .dropdown-menu {
right: 0;

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

@ -201,11 +201,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
let $row = $(`<div class="list-item">
<div class="list-item__content" style="flex: 0 0 10px;">
<input type="checkbox" class="list-row-check" ${result.checked ? 'checked' : ''}>
<input type="checkbox" class="list-row-check" data-item-name="${result.name}" ${result.checked ? 'checked' : ''}>
</div>
${contents}
</div>`);
head ? $row.addClass('list-item--head')
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row);
return $row;
@ -219,14 +220,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
if (!frappe.flags.auto_scroll) {
this.empty_list();
}
more_btn.hide();
if(results.length === 0) {
this.empty_list();
more_btn.hide();
return;
} else if(more) {
more_btn.show();
}
if (results.length === 0) return;
if (more) more_btn.show();
results.forEach((result) => {
me.$results.append(me.make_list_row(result));

View file

@ -984,7 +984,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
setup_new_doc_event() {
this.$no_result.find('.btn-new-doc').click(() => this.make_new_doc());
this.$no_result.find('.btn-new-doc').click(() => {
if (this.settings.primary_action) {
this.settings.primary_action();
} else {
this.make_new_doc();
}
});
}
setup_tag_event() {

View file

@ -354,7 +354,7 @@ frappe.views.FileView.grid_view = frappe.get_user_settings('File').grid_view ||
function redirect_to_home_if_invalid_route() {
const route = frappe.get_route();
if (route[2] !== 'Home') {
if (route[2] === 'List') {
// if the user somehow redirects to List/File/List
// redirect back to Home
frappe.set_route('List', 'File', 'Home');

View file

@ -98,6 +98,9 @@ frappe.views.Page = Class.extend({
this.wrapper.innerHTML = this.pagedoc.content;
frappe.dom.eval(this.pagedoc.__script || this.pagedoc.script || '');
frappe.dom.set_style(this.pagedoc.style || '');
// set breadcrumbs
frappe.breadcrumbs.add(this.pagedoc.module || null);
}
this.trigger_page_event('on_page_load');

View file

@ -313,4 +313,4 @@ frappe.ui.WebFormListRow = class WebFormListRow {
is_selected() {
return this.checkbox.checked;
}
};
};

View file

@ -33,9 +33,15 @@
{% for row in data %}
<tr>
{% for col in columns %}
<td {{- get_alignment(col) }}>
{{- frappe.format(row[col.fieldname], col, row) -}}
</td>
{% if row[col.fieldname] == 'Total' %}
<td {{- get_alignment(col) }}>
{{- row[col.fieldname] -}}
</td>
{% else %}
<td {{- get_alignment(col) }}>
{{- frappe.format(row[col.fieldname], col, row) -}}
</td>
{% endif %}
{% endfor %}
</td>
{% endfor %}

View file

@ -21,7 +21,7 @@ login.bind_events = function() {
args.pwd = $("#login_password").val();
args.device = "desktop";
if(!args.usr || !args.pwd) {
frappe.msgprint("{{ _("Both login and password required") }}");
frappe.msgprint('{{ _("Both login and password required") }}');
return false;
}
login.call(args);
@ -36,7 +36,7 @@ login.bind_events = function() {
args.redirect_to = frappe.utils.get_url_arg("redirect-to") || '';
args.full_name = ($("#signup_fullname").val() || "").trim();
if(!args.email || !validate_email(args.email) || !args.full_name) {
login.set_indicator("{{ _("Valid email and name required") }}", 'red');
login.set_indicator('{{ _("Valid email and name required") }}', 'red');
return false;
}
login.call(args);
@ -49,7 +49,7 @@ login.bind_events = function() {
args.cmd = "frappe.core.doctype.user.user.reset_password";
args.user = ($("#forgot_email").val() || "").trim();
if(!args.user) {
login.set_indicator("{{ _("Valid Login id required.") }}", 'red');
login.set_indicator('{{ _("Valid Login id required.") }}', 'red');
return false;
}
login.call(args);
@ -74,7 +74,7 @@ login.bind_events = function() {
args.pwd = $("#login_password").val();
args.device = "desktop";
if(!args.usr || !args.pwd) {
login.set_indicator("{{ _("Both login and password required") }}", 'red');
login.set_indicator('{{ _("Both login and password required") }}', 'red');
return false;
}
login.call(args);
@ -125,7 +125,7 @@ login.signup = function() {
// Login
login.call = function(args, callback) {
login.set_indicator("{{ _('Verifying...') }}", 'blue');
login.set_indicator('{{ _("Verifying...") }}', 'blue');
return frappe.call({
type: "POST",
@ -172,7 +172,7 @@ login.login_handlers = (function() {
var login_handlers = {
200: function(data) {
if(data.message == 'Logged In'){
login.set_indicator("{{ _("Success") }}", 'green');
login.set_indicator('{{ _("Success") }}', 'green');
window.location.href = frappe.utils.get_url_arg("redirect-to") || data.home_page;
} else if(data.message == 'Password Reset'){
window.location.href = data.redirect_to;
@ -196,13 +196,13 @@ login.login_handlers = (function() {
}
} else if(window.location.hash === '#forgot') {
if(data.message==='not found') {
login.set_indicator("{{ _("Not a valid user") }}", 'red');
login.set_indicator('{{ _("Not a valid user") }}', 'red');
} else if (data.message=='not allowed') {
login.set_indicator("{{ _("Not Allowed") }}", 'red');
login.set_indicator('{{ _("Not Allowed") }}', 'red');
} else if (data.message=='disabled') {
login.set_indicator("{{ _("Not Allowed: Disabled User") }}", 'red');
login.set_indicator('{{ _("Not Allowed: Disabled User") }}', 'red');
} else {
login.set_indicator("{{ _("Instructions Emailed") }}", 'green');
login.set_indicator('{{ _("Instructions Emailed") }}', 'green');
}
@ -210,7 +210,7 @@ login.login_handlers = (function() {
if(cint(data.message[0])==0) {
login.set_indicator(data.message[1], 'red');
} else {
login.set_indicator("{{ _('Success') }}", 'green');
login.set_indicator('{{ _("Success") }}', 'green');
frappe.msgprint(data.message[1])
}
//login.set_indicator(__(data.message), 'green');
@ -218,7 +218,7 @@ login.login_handlers = (function() {
//OTP verification
if(data.verification && data.message != 'Logged In') {
login.set_indicator("{{ _("Success") }}", 'green');
login.set_indicator('{{ _("Success") }}', 'green');
document.cookie = "tmp_id="+data.tmp_id;
@ -231,8 +231,8 @@ login.login_handlers = (function() {
}
}
},
401: get_error_handler("{{ _("Invalid Login. Try again.") }}"),
417: get_error_handler("{{ _("Oops! Something went wrong") }}")
401: get_error_handler('{{ _("Invalid Login. Try again.") }}'),
417: get_error_handler('{{ _("Oops! Something went wrong") }}')
};
return login_handlers;
@ -272,11 +272,11 @@ var request_otp = function(r){
$('.login-content').empty().append($('<div>').attr({'id':'twofactor_div'}).html(
'<form class="form-verify">\
<div class="page-card-head">\
<span class="indicator blue" data-text="Verification">Verification</span>\
<span class="indicator blue" data-text="Verification">{{ _("Verification") }}</span>\
</div>\
<div id="otp_div"></div>\
<input type="text" id="login_token" autocomplete="off" class="form-control" placeholder="Verification Code" required="" autofocus="">\
<button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button>\
<input type="text" id="login_token" autocomplete="off" class="form-control" placeholder={{ _("Verification Code") }} required="" autofocus="">\
<button class="btn btn-sm btn-primary btn-block" id="verify_token">{{ _("Verify") }}</button>\
</form>'));
// add event handler for submit button
verify_token();
@ -287,11 +287,11 @@ var continue_otp_app = function(setup, qrcode){
var qrcode_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');
if (setup){
direction = $('<div>').attr('id','qr_info').text('Enter Code displayed in OTP App.');
direction = $('<div>').attr('id','qr_info').text('{{ _("Enter Code displayed in OTP App.") }}');
qrcode_div.append(direction);
$('#otp_div').prepend(qrcode_div);
} else {
direction = $('<div>').attr('id','qr_info').text('OTP setup using OTP App was not completed. Please contact Administrator.');
direction = $('<div>').attr('id','qr_info').text('{{ _("OTP setup using OTP App was not completed. Please contact Administrator.") }}');
qrcode_div.append(direction);
$('#otp_div').prepend(qrcode_div);
}
@ -305,7 +305,7 @@ var continue_sms = function(setup, prompt){
sms_div.append(prompt)
$('#otp_div').prepend(sms_div);
} else {
direction = $('<div>').attr('id','qr_info').text(prompt || 'SMS was not sent. Please contact Administrator.');
direction = $('<div>').attr('id','qr_info').text(prompt || '{{ _("SMS was not sent. Please contact Administrator.") }}');
sms_div.append(direction);
$('#otp_div').prepend(sms_div)
}
@ -319,7 +319,7 @@ var continue_email = function(setup, prompt){
email_div.append(prompt)
$('#otp_div').prepend(email_div);
} else {
var direction = $('<div>').attr('id','qr_info').text(prompt || 'Verification code email not sent. Please contact Administrator.');
var direction = $('<div>').attr('id','qr_info').text(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}');
email_div.append(direction);
$('#otp_div').prepend(email_div);
}

View file

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

View file

@ -58,7 +58,7 @@ def sanitize_html(html, linkify=False):
return html
tags = (acceptable_elements + svg_elements + mathml_elements
+ ["html", "head", "meta", "link", "body", "iframe", "style", "o:p"])
+ ["html", "head", "meta", "link", "body", "style", "o:p"])
attributes = {"*": acceptable_attributes, 'svg': svg_attributes}
styles = bleach_whitelist.all_styles
strip_comments = False

View file

@ -13,7 +13,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 .
@ -62,13 +62,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

@ -87,7 +87,7 @@
<span class="indicator blue" data-text="{{ _("Forgot Password") }}"></span></div>
<input type="email" id="forgot_email"
class="form-control" placeholder="{{ _('Email address') }}" required autofocus>
<button class="btn btn-sm btn-primary btn-block btn-forgot" type="submit">{{ _("Send Password") }}</button>
<button class="btn btn-sm btn-primary btn-block btn-forgot" type="submit">{{ _("Reset Password") }}</button>
</form>
</div>
<div class='form-footer'>