google_calendar.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from uuid import uuid4
  4. import requests
  5. import json
  6. import logging
  7. from odoo import fields
  8. from odoo.addons.google_calendar.utils.google_event import GoogleEvent
  9. from odoo.addons.google_account.models.google_service import TIMEOUT
  10. _logger = logging.getLogger(__name__)
  11. def requires_auth_token(func):
  12. def wrapped(self, *args, **kwargs):
  13. if not kwargs.get('token'):
  14. raise AttributeError("An authentication token is required")
  15. return func(self, *args, **kwargs)
  16. return wrapped
  17. class InvalidSyncToken(Exception):
  18. pass
  19. class GoogleCalendarService():
  20. def __init__(self, google_service):
  21. self.google_service = google_service
  22. @requires_auth_token
  23. def get_events(self, sync_token=None, token=None, timeout=TIMEOUT):
  24. url = "/calendar/v3/calendars/primary/events"
  25. headers = {'Content-type': 'application/json'}
  26. params = {'access_token': token}
  27. if sync_token:
  28. params['syncToken'] = sync_token
  29. else:
  30. # full sync, limit to a range of 1y in past to 1y in the futur by default
  31. ICP = self.google_service.env['ir.config_parameter'].sudo()
  32. day_range = int(ICP.get_param('google_calendar.sync.range_days', default=365))
  33. _logger.info("Full cal sync, restricting to %s days range", day_range)
  34. lower_bound = fields.Datetime.subtract(fields.Datetime.now(), days=day_range)
  35. upper_bound = fields.Datetime.add(fields.Datetime.now(), days=day_range)
  36. params['timeMin'] = lower_bound.isoformat() + 'Z' # Z = UTC (RFC3339)
  37. params['timeMax'] = upper_bound.isoformat() + 'Z' # Z = UTC (RFC3339)
  38. try:
  39. status, data, time = self.google_service._do_request(url, params, headers, method='GET', timeout=timeout)
  40. except requests.HTTPError as e:
  41. if e.response.status_code == 410 and 'fullSyncRequired' in str(e.response.content):
  42. raise InvalidSyncToken("Invalid sync token. Full sync required")
  43. raise e
  44. events = data.get('items', [])
  45. next_page_token = data.get('nextPageToken')
  46. while next_page_token:
  47. params = {'access_token': token, 'pageToken': next_page_token}
  48. status, data, time = self.google_service._do_request(url, params, headers, method='GET', timeout=timeout)
  49. next_page_token = data.get('nextPageToken')
  50. events += data.get('items', [])
  51. next_sync_token = data.get('nextSyncToken')
  52. default_reminders = data.get('defaultReminders')
  53. return GoogleEvent(events), next_sync_token, default_reminders
  54. @requires_auth_token
  55. def insert(self, values, token=None, timeout=TIMEOUT):
  56. send_updates = self.google_service._context.get('send_updates', True)
  57. url = "/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=%s" % ("all" if send_updates else "none")
  58. headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
  59. if not values.get('id'):
  60. values['id'] = uuid4().hex
  61. self.google_service._do_request(url, json.dumps(values), headers, method='POST', timeout=timeout)
  62. return values['id']
  63. @requires_auth_token
  64. def patch(self, event_id, values, token=None, timeout=TIMEOUT):
  65. url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
  66. headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
  67. self.google_service._do_request(url, json.dumps(values), headers, method='PATCH', timeout=timeout)
  68. @requires_auth_token
  69. def delete(self, event_id, token=None, timeout=TIMEOUT):
  70. url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
  71. headers = {'Content-type': 'application/json'}
  72. params = {'access_token': token}
  73. # Delete all events from recurrence in a single request to Google and triggering a single mail.
  74. # The 'singleEvents' parameter is a trick that tells Google API to delete all recurrent events individually,
  75. # making the deletion be handled entirely on their side, and then we archive the events in Odoo.
  76. is_recurrence = self.google_service._context.get('is_recurrence', True)
  77. if is_recurrence:
  78. params['singleEvents'] = 'true'
  79. try:
  80. self.google_service._do_request(url, params, headers=headers, method='DELETE', timeout=timeout)
  81. except requests.HTTPError as e:
  82. # For some unknown reason Google can also return a 403 response when the event is already cancelled.
  83. if e.response.status_code not in (410, 403):
  84. raise e
  85. _logger.info("Google event %s was already deleted" % event_id)
  86. #################################
  87. ## MANAGE CONNEXION TO GMAIL ##
  88. #################################
  89. def is_authorized(self, user):
  90. return bool(user.sudo().google_calendar_rtoken)
  91. def _get_calendar_scope(self, RO=False):
  92. readonly = '.readonly' if RO else ''
  93. return 'https://www.googleapis.com/auth/calendar%s' % (readonly)
  94. def _google_authentication_url(self, from_url='http://www.odoo.com'):
  95. state = {
  96. 'd': self.google_service.env.cr.dbname,
  97. 's': 'calendar',
  98. 'f': from_url
  99. }
  100. base_url = self.google_service._context.get('base_url') or self.google_service.get_base_url()
  101. return self.google_service._get_authorize_uri(
  102. 'calendar',
  103. self._get_calendar_scope(),
  104. base_url + '/google_account/authentication',
  105. state=json.dumps(state),
  106. approval_prompt='force',
  107. access_type='offline'
  108. )
  109. def _can_authorize_google(self, user):
  110. return user.has_group('base.group_erp_manager')