diff --git a/frappe/__init__.py b/frappe/__init__.py index c19295a95c..ce03a6a704 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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() diff --git a/frappe/change_log/v12/v12_0_0.md b/frappe/change_log/v12/v12_0_0.md new file mode 100644 index 0000000000..215b612794 --- /dev/null +++ b/frappe/change_log/v12/v12_0_0.md @@ -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) diff --git a/frappe/chat/doctype/chat_room/chat_room.py b/frappe/chat/doctype/chat_room/chat_room.py index d0085bfab5..44a6ce0f0b 100644 --- a/frappe/chat/doctype/chat_room/chat_room.py +++ b/frappe/chat/doctype/chat_room/chat_room.py @@ -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: diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 1eec4525fa..e28fd36346 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -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: diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 224fa65d13..bbe3966b29 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -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."), } ] } diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index dc2f9194b9..5500f1c617 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -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: diff --git a/frappe/core/doctype/data_import/test_data_import.py b/frappe/core/doctype/data_import/test_data_import.py index b0b41a1ced..406ea08958 100644 --- a/frappe/core/doctype/data_import/test_data_import.py +++ b/frappe/core/doctype/data_import/test_data_import.py @@ -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")) \ No newline at end of file diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 3c3543e1dd..847e0cd267 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -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("{file_name}{icon}".format(**{ "icon": ' ' 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 }))) diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 8c1dd51be3..763fd16c50 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -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() diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py index d265b1ae00..97aa69fd9c 100644 --- a/frappe/core/doctype/version/test_version.py +++ b/frappe/core/doctype/version/test_version.py @@ -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] diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index 07b5c16451..14b50d0388 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -3,9 +3,9 @@ - - - + + + @@ -28,13 +28,13 @@
Queue / WorkerJobCreated{{ _("Queue / Worker") }}{{ _("Job") }}{{ _("Created") }}

- Started - Queued - Failed - Finished + {{ _("Started") }} + {{ _("Queued") }} + {{ _("Failed") }} + {{ _("Finished") }}

{% else %} -

No pending or current jobs for this site

+

{{ _("No pending or current jobs for this site") }}

{% endif %} -

Last refreshed {{ frappe.datetime.now_datetime() }}

- \ No newline at end of file +

{{ _("Last refreshed") }} {{ frappe.datetime.now_datetime() }}

+ diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js index 2bde8454a9..bbc8bf049b 100644 --- a/frappe/core/page/background_jobs/background_jobs.js +++ b/frappe/core/page/background_jobs/background_jobs.js @@ -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 }); diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 4547e81e4e..1cd71ea05d 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -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") diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 610f15e011..b72f5b8380 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -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 diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 38635daebe..f42faea0e5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -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 = [ diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 0273fe80b1..73677b1a95 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -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: diff --git a/frappe/desk/doctype/event/event.js b/frappe/desk/doctype/event/event.js index a3adede558..87d78bae94 100644 --- a/frappe/desk/doctype/event/event.js +++ b/frappe/desk/doctype/event/event.js @@ -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) { diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index ea36a8c454..032030ddef 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -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", diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 92de672983..b8527f06f9 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -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() diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index 50e994252e..f99536f9a8 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -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): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index dcd21d3c10..a95975c4b0 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -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 diff --git a/frappe/hooks.py b/frappe/hooks.py index e6fdf58b1f..41f805f2b6 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -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", diff --git a/frappe/installer.py b/frappe/installer.py index 4c97ffd8fc..764a0b6780 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -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 diff --git a/frappe/integrations/data_migration_mapping/event_to_gcalendar/__init__.py b/frappe/integrations/data_migration_mapping/event_to_gcalendar/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json b/frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json deleted file mode 100644 index 6d2a6020a5..0000000000 --- a/frappe/integrations/data_migration_mapping/event_to_gcalendar/event_to_gcalendar.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json b/frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json deleted file mode 100644 index a4ca740ce5..0000000000 --- a/frappe/integrations/data_migration_mapping/gcalendar_to_event/gcalendar_to_event.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json b/frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json deleted file mode 100644 index 9eef669203..0000000000 --- a/frappe/integrations/data_migration_plan/gcalendar_sync/gcalendar_sync.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/frappe/integrations/doctype/gcalendar_account/__init__.py b/frappe/integrations/doctype/gcalendar_account/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.js b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.js deleted file mode 100644 index d8ad7d46ad..0000000000 --- a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.js +++ /dev/null @@ -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); - } - } - }); - } -}); diff --git a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.json b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.json deleted file mode 100644 index d531d32862..0000000000 --- a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.json +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.py b/frappe/integrations/doctype/gcalendar_account/gcalendar_account.py deleted file mode 100644 index bc2647dd49..0000000000 --- a/frappe/integrations/doctype/gcalendar_account/gcalendar_account.py +++ /dev/null @@ -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() diff --git a/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js b/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js deleted file mode 100644 index 580b240c49..0000000000 --- a/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.js +++ /dev/null @@ -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() - ]); - -}); diff --git a/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py b/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py deleted file mode 100644 index 6ed4a95b12..0000000000 --- a/frappe/integrations/doctype/gcalendar_account/test_gcalendar_account.py +++ /dev/null @@ -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)) diff --git a/frappe/integrations/doctype/gcalendar_settings/__init__.py b/frappe/integrations/doctype/gcalendar_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js deleted file mode 100644 index 3be5603b9b..0000000000 --- a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.js +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2017, DOKOS and contributors -// For license information, please see license.txt - -frappe.ui.form.on('GCalendar Settings', { - - -}); diff --git a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json deleted file mode 100644 index 05111ec791..0000000000 --- a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.json +++ /dev/null @@ -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 -} diff --git a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py b/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py deleted file mode 100644 index 0eff2b0f98..0000000000 --- a/frappe/integrations/doctype/gcalendar_settings/gcalendar_settings.py +++ /dev/null @@ -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() diff --git a/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js b/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js deleted file mode 100644 index 23bd41ff9b..0000000000 --- a/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.js +++ /dev/null @@ -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() - ]); - -}); diff --git a/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py b/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py deleted file mode 100644 index cbc42a1c5f..0000000000 --- a/frappe/integrations/doctype/gcalendar_settings/test_gcalendar_settings.py +++ /dev/null @@ -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 diff --git a/frappe/integrations/data_migration_mapping/__init__.py b/frappe/integrations/doctype/google_calendar/__init__.py similarity index 100% rename from frappe/integrations/data_migration_mapping/__init__.py rename to frappe/integrations/doctype/google_calendar/__init__.py diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.js b/frappe/integrations/doctype/google_calendar/google_calendar.js new file mode 100644 index 0000000000..c62e59fdbe --- /dev/null +++ b/frappe/integrations/doctype/google_calendar/google_calendar.js @@ -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 Google Settings.")); + } + + 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); + } + } + }); + } +}); diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.json b/frappe/integrations/doctype/google_calendar/google_calendar.json new file mode 100644 index 0000000000..2c34e6967a --- /dev/null +++ b/frappe/integrations/doctype/google_calendar/google_calendar.json @@ -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 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py new file mode 100644 index 0000000000..cc503db9f7 --- /dev/null +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -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']""" diff --git a/frappe/integrations/doctype/google_calendar/test_google_calendar.py b/frappe/integrations/doctype/google_calendar/test_google_calendar.py new file mode 100644 index 0000000000..0fad81d7f5 --- /dev/null +++ b/frappe/integrations/doctype/google_calendar/test_google_calendar.py @@ -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 diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.js b/frappe/integrations/doctype/google_contacts/google_contacts.js index acd5fb35ae..911c16ea91 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.js +++ b/frappe/integrations/doctype/google_contacts/google_contacts.js @@ -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); diff --git a/frappe/integrations/doctype/google_settings/google_settings.js b/frappe/integrations/doctype/google_settings/google_settings.js index 488cb05b9b..0d2ede9b9b 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.js +++ b/frappe/integrations/doctype/google_settings/google_settings.js @@ -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) { + // } }); diff --git a/frappe/integrations/doctype/google_settings/google_settings.json b/frappe/integrations/doctype/google_settings/google_settings.json index 8a316fd67e..086c56c020 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.json +++ b/frappe/integrations/doctype/google_settings/google_settings.json @@ -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", diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 87d08bbfbb..8de3ec1834 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -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): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index f864e4f356..8a245b626e 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -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: diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py deleted file mode 100644 index 16ccf284d5..0000000000 --- a/frappe/model/db_schema.py +++ /dev/null @@ -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))) diff --git a/frappe/patches/v12_0/remove_gcalendar_gmaps.py b/frappe/patches/v12_0/remove_gcalendar_gmaps.py new file mode 100644 index 0000000000..84c400f6a8 --- /dev/null +++ b/frappe/patches/v12_0/remove_gcalendar_gmaps.py @@ -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') diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 160af10c6e..8605fb762e 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -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(); diff --git a/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html b/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html index 5397d5252f..697f27866e 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_column_selector.html @@ -1,5 +1,8 @@

{{ __("Check columns to select, drag to set order.") }} -
{{ __("Widths can be set in px or %.") }}

+ {{ __("Widths can be set in px or %.") }}

+

+ {{ __("Some columns might get cut off when printing to PDF. Try to keep number of columns under 10.") }} +

{{ __("Column") }}

{{ __("Width") }}

diff --git a/frappe/public/css/bootstrap.css b/frappe/public/css/bootstrap.css index c4db43f199..a8e6c87cda 100644 --- a/frappe/public/css/bootstrap.css +++ b/frappe/public/css/bootstrap.css @@ -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; diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 68136eba44..cc4d3c952a 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -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({ diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 2035b21be3..5d8af93549 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -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"); diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index d2caa24e43..2621448af9 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -201,11 +201,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ let $row = $(`
- +
${contents}
`); + head ? $row.addClass('list-item--head') : $row = $(`
`).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)); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 034cd91b42..4209899bd3 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -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() { diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js index fb01b87a61..6ee37aec3d 100644 --- a/frappe/public/js/frappe/views/file/file_view.js +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -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'); diff --git a/frappe/public/js/frappe/views/pageview.js b/frappe/public/js/frappe/views/pageview.js index c31ba74285..c0149672cb 100644 --- a/frappe/public/js/frappe/views/pageview.js +++ b/frappe/public/js/frappe/views/pageview.js @@ -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'); diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 76d37bd39a..7b14844c6f 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -313,4 +313,4 @@ frappe.ui.WebFormListRow = class WebFormListRow { is_selected() { return this.checkbox.checked; } -}; \ No newline at end of file +}; diff --git a/frappe/templates/emails/auto_email_report.html b/frappe/templates/emails/auto_email_report.html index 50ab5b1fe3..a658c988a9 100644 --- a/frappe/templates/emails/auto_email_report.html +++ b/frappe/templates/emails/auto_email_report.html @@ -33,9 +33,15 @@ {% for row in data %} {% for col in columns %} - - {{- frappe.format(row[col.fieldname], col, row) -}} - + {% if row[col.fieldname] == 'Total' %} + + {{- row[col.fieldname] -}} + + {% else %} + + {{- frappe.format(row[col.fieldname], col, row) -}} + + {% endif %} {% endfor %} {% endfor %} diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 519a49220a..63ec6588e5 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -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($('
').attr({'id':'twofactor_div'}).html( '
\
\ - Verification\ + {{ _("Verification") }}\
\
\ - \ - \ + \ + \
')); // add event handler for submit button verify_token(); @@ -287,11 +287,11 @@ var continue_otp_app = function(setup, qrcode){ var qrcode_div = $('
'); if (setup){ - direction = $('
').attr('id','qr_info').text('Enter Code displayed in OTP App.'); + direction = $('
').attr('id','qr_info').text('{{ _("Enter Code displayed in OTP App.") }}'); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } else { - direction = $('
').attr('id','qr_info').text('OTP setup using OTP App was not completed. Please contact Administrator.'); + direction = $('
').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 = $('
').attr('id','qr_info').text(prompt || 'SMS was not sent. Please contact Administrator.'); + direction = $('
').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 = $('
').attr('id','qr_info').text(prompt || 'Verification code email not sent. Please contact Administrator.'); + var direction = $('
').attr('id','qr_info').text(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}'); email_div.append(direction); $('#otp_div').prepend(email_div); } diff --git a/frappe/templates/styles/standard.css b/frappe/templates/styles/standard.css index 39ea31a805..bedea51c41 100644 --- a/frappe/templates/styles/standard.css +++ b/frappe/templates/styles/standard.css @@ -166,3 +166,6 @@ table td div { max-height: 150px; } +.print-format [data-fieldtype="Table"] { + overflow: auto; +} diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py index 2c3364660b..1dd06ffb3a 100644 --- a/frappe/utils/html_utils.py +++ b/frappe/utils/html_utils.py @@ -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 diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 3ad97afe8b..a31fb9922a 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -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) diff --git a/frappe/www/login.html b/frappe/www/login.html index d621ab0fbb..8c470ac6dd 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -87,7 +87,7 @@
- +