Merge branch 'version-12-hotfix' into merge_v12_hotfix_1
This commit is contained in:
commit
9b65a7c4a1
67 changed files with 1074 additions and 2168 deletions
|
|
@ -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()
|
||||
|
|
|
|||
29
frappe/change_log/v12/v12_0_0.md
Normal file
29
frappe/change_log/v12/v12_0_0.md
Normal 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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
@ -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
|
||||
})))
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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))
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Copyright (c) 2017, DOKOS and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('GCalendar Settings', {
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
146
frappe/integrations/doctype/google_calendar/google_calendar.json
Normal file
146
frappe/integrations/doctype/google_calendar/google_calendar.json
Normal 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
|
||||
}
|
||||
594
frappe/integrations/doctype/google_calendar/google_calendar.py
Normal file
594
frappe/integrations/doctype/google_calendar/google_calendar.py
Normal 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']"""
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
10
frappe/patches/v12_0/remove_gcalendar_gmaps.py
Normal file
10
frappe/patches/v12_0/remove_gcalendar_gmaps.py
Normal 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')
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
2
frappe/public/css/bootstrap.css
vendored
2
frappe/public/css/bootstrap.css
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -313,4 +313,4 @@ frappe.ui.WebFormListRow = class WebFormListRow {
|
|||
is_selected() {
|
||||
return this.checkbox.checked;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,3 +166,6 @@ table td div {
|
|||
max-height: 150px;
|
||||
}
|
||||
|
||||
.print-format [data-fieldtype="Table"] {
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue