[IMP] Improve Calendar synchronization

- Use the new fields from calendar (renamed in 9398.1.7 trunk-cal-v8-jke)
    - Manage some google excpetions about token expiration
    - Manage some google excpetions about token revokation
    - Manage some google excpetions about 410 - Gone from google
    - Detect / Allow user to change the calendar and remove all old reference.
    - Use by default the since_modified for synchronization and not always a full (updatedTime in google)
    - Add some key for ir_parameters : 
             'calendar.week_synchro' : number of week to synchornize when we are in full mode
             'calendar.block_synchro_attendee' : need to synchronize the attendees between google and openerp (creation of partner)
             'calendar.block_since_synchro' : Always use the full synchro. (Can be forced in "one time" removing the last synchro from res_users form)
             'calendar.debug_print' : _logger is more verbose
    - Pep 8
    - Change behaviour of do_request to return a tuple with status raquest and response
    - Add field from token and synchro date in res_users view form
This commit is contained in:
Kersten Jeremy 2014-05-14 10:28:10 +02:00
parent 7564f528e0
commit 02be7aa65c
7 changed files with 498 additions and 267 deletions

View File

@ -1,23 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011-2014 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import pytz import pytz
import re import re
@ -188,7 +169,8 @@ class calendar_attendee(osv.Model):
res = cal.serialize() res = cal.serialize()
return res return res
def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False), template_xmlid='calendar_template_meeting_invitation', context=None): def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False),
template_xmlid='calendar_template_meeting_invitation', context=None):
""" """
Send mail for event invitation to event attendees. Send mail for event invitation to event attendees.
@param email_from: email address for user sending the mail @param email_from: email address for user sending the mail
@ -273,7 +255,8 @@ class calendar_attendee(osv.Model):
meeting_obj = self.pool['calendar.event'] meeting_obj = self.pool['calendar.event']
res = self.write(cr, uid, ids, {'state': 'accepted'}, context) res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
for attendee in self.browse(cr, uid, ids, context=context): for attendee in self.browse(cr, uid, ids, context=context):
meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context) meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)),
subtype="calendar.subtype_invitation", context=context)
return res return res
@ -1410,7 +1393,7 @@ class calendar_event(osv.Model):
new_args.append(new_arg) new_args.append(new_arg)
if not context.get('virtual_id', True): if not context.get('virtual_id', True):
return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count) return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, count=count, context=context)
# offset, limit, order and count must be treated separately as we may need to deal with virtual ids # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False) res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
@ -1535,17 +1518,17 @@ class calendar_event(osv.Model):
if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True): if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True):
the_id = new_id or (ids and int(ids[0])) the_id = new_id or (ids and int(ids[0]))
if the_id:
if attendees_create:
attendees_create = attendees_create[the_id]
mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
else:
mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
if attendees_create: if mail_to_ids:
attendees_create = attendees_create[the_id] current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids'])) if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context):
else: self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context)
mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
if mail_to_ids:
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context):
self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context)
return res or True and False return res or True and False
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
@ -1624,7 +1607,7 @@ class calendar_event(osv.Model):
continue continue
if r['class'] == 'private': if r['class'] == 'private':
for f in r.keys(): for f in r.keys():
if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'): if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date', 'rrule'):
if isinstance(r[f], list): if isinstance(r[f], list):
r[f] = [] r[f] = []
else: else:

View File

@ -1,28 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import openerp
from openerp.http import request
from openerp.osv import osv from openerp.osv import osv
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.addons.web.http import request from datetime import datetime
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
import werkzeug.urls import werkzeug.urls
import urllib2 import urllib2
@ -31,6 +15,7 @@ import simplejson
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class google_service(osv.osv_memory): class google_service(osv.osv_memory):
_name = 'google.service' _name = 'google.service'
@ -48,7 +33,8 @@ class google_service(osv.osv_memory):
req = urllib2.Request("https://accounts.google.com/o/oauth2/token", data, headers) req = urllib2.Request("https://accounts.google.com/o/oauth2/token", data, headers)
content = urllib2.urlopen(req).read() content = urllib2.urlopen(req).read()
except urllib2.HTTPError: except urllib2.HTTPError:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) error_msg = "Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"
raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
content = simplejson.loads(content) content = simplejson.loads(content)
return content.get('refresh_token') return content.get('refresh_token')
@ -65,8 +51,8 @@ class google_service(osv.osv_memory):
uri = 'https://accounts.google.com/o/oauth2/auth?%s' % werkzeug.url_encode(params) uri = 'https://accounts.google.com/o/oauth2/auth?%s' % werkzeug.url_encode(params)
return uri return uri
#If no scope is passed, we use service by default to get a default scope # If no scope is passed, we use service by default to get a default scope
def _get_authorize_uri(self, cr, uid, from_url, service, scope = False, context=None): def _get_authorize_uri(self, cr, uid, from_url, service, scope=False, context=None):
""" This method return the url needed to allow this instance of OpenErp to access to the scope of gmail specified as parameters """ """ This method return the url needed to allow this instance of OpenErp to access to the scope of gmail specified as parameters """
state_obj = dict(d=cr.dbname, s=service, f=from_url) state_obj = dict(d=cr.dbname, s=service, f=from_url)
@ -76,11 +62,11 @@ class google_service(osv.osv_memory):
params = { params = {
'response_type': 'code', 'response_type': 'code',
'client_id': client_id, 'client_id': client_id,
'state' : simplejson.dumps(state_obj), 'state': simplejson.dumps(state_obj),
'scope': scope or 'https://www.googleapis.com/auth/%s' % (service,), 'scope': scope or 'https://www.googleapis.com/auth/%s' % (service,),
'redirect_uri': base_url + '/google_account/authentication', 'redirect_uri': base_url + '/google_account/authentication',
'approval_prompt':'force', 'approval_prompt': 'force',
'access_type':'offline' 'access_type': 'offline'
} }
uri = self.get_uri_oauth(a='auth') + "?%s" % werkzeug.url_encode(params) uri = self.get_uri_oauth(a='auth') + "?%s" % werkzeug.url_encode(params)
@ -96,25 +82,24 @@ class google_service(osv.osv_memory):
'code': authorize_code, 'code': authorize_code,
'client_id': client_id, 'client_id': client_id,
'client_secret': client_secret, 'client_secret': client_secret,
'grant_type' : 'authorization_code', 'grant_type': 'authorization_code',
'redirect_uri': base_url + '/google_account/authentication' 'redirect_uri': base_url + '/google_account/authentication'
} }
headers = {"content-type": "application/x-www-form-urlencoded"} headers = {"content-type": "application/x-www-form-urlencoded"}
try: try:
uri = self.get_uri_oauth(a='token')
data = werkzeug.url_encode(params) data = werkzeug.url_encode(params)
req = urllib2.Request(self.get_uri_oauth(a='token'), data, headers)
content = urllib2.urlopen(req).read() st, res = self._do_request(cr, uid, uri, params=data, headers=headers, type='POST', preuri='', context=context)
res = simplejson.loads(content) except urllib2.HTTPError:
except urllib2.HTTPError,e: error_msg = "Something went wrong during your token generation. Maybe your Authorization Code is invalid"
raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid"), context=context) raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
return res return res
def _refresh_google_token_json(self, cr, uid, refresh_token, service, context=None): #exchange_AUTHORIZATION vs Token (service = calendar) def _refresh_google_token_json(self, cr, uid, refresh_token, service, context=None): # exchange_AUTHORIZATION vs Token (service = calendar)
res = False res = False
base_url = self.get_base_url(cr, uid, context)
client_id = self.get_client_id(cr, uid, service, context) client_id = self.get_client_id(cr, uid, service, context)
client_secret = self.get_client_secret(cr, uid, service, context) client_secret = self.get_client_secret(cr, uid, service, context)
@ -122,60 +107,79 @@ class google_service(osv.osv_memory):
'refresh_token': refresh_token, 'refresh_token': refresh_token,
'client_id': client_id, 'client_id': client_id,
'client_secret': client_secret, 'client_secret': client_secret,
'grant_type' : 'refresh_token' 'grant_type': 'refresh_token',
} }
headers = {"content-type": "application/x-www-form-urlencoded"} headers = {"content-type": "application/x-www-form-urlencoded"}
try: try:
data = werkzeug.url_encode(params) uri = self.get_uri_oauth(a='token')
req = urllib2.Request(self.get_uri_oauth(a='token'), data, headers)
content = urllib2.urlopen(req).read()
res = simplejson.loads(content)
except urllib2.HTTPError:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context)
data = werkzeug.url_encode(params)
st, res = self._do_request(cr, uid, uri, params=data, headers=headers, type='POST', preuri='', context=context)
except urllib2.HTTPError, e:
if e.code == 400: # invalid grant
registry = openerp.modules.registry.RegistryManager.get(request.session.db)
with registry.cursor() as cur:
self.pool['res.users'].write(cur, uid, [uid], {'google_%s_rtoken' % service: False}, context=context)
error_key = simplejson.loads(e.read()).get("error", "nc")
_logger.exception("Bad google request : %s !" % error_key)
error_msg = "Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]" % error_key
raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
return res return res
def _do_request(self, cr, uid, uri, params={}, headers={}, type='POST', preuri="https://www.googleapis.com", context=None):
if context is None:
context = {}
def _do_request(self,cr,uid,uri,params={},headers={},type='POST', context=None): """ Return a tuple ('HTTP_CODE', 'HTTP_RESPONSE') """
_logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !" % (uri,type,headers,werkzeug.url_encode(params) if type =='GET' else params)) _logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !" % (uri, type, headers, werkzeug.url_encode(params) if type == 'GET' else params))
res = False
status = 418
response = ""
try: try:
if type.upper() == 'GET' or type.upper() == 'DELETE': if type.upper() == 'GET' or type.upper() == 'DELETE':
data = werkzeug.url_encode(params) data = werkzeug.url_encode(params)
req = urllib2.Request(self.get_uri_api() + uri + "?" + data) req = urllib2.Request(preuri + uri + "?" + data)
elif type.upper() == 'POST' or type.upper() == 'PATCH' or type.upper() == 'PUT': elif type.upper() == 'POST' or type.upper() == 'PATCH' or type.upper() == 'PUT':
req = urllib2.Request(self.get_uri_api() + uri, params, headers) req = urllib2.Request(preuri + uri, params, headers)
else: else:
raise ('Method not supported [%s] not in [GET, POST, PUT, PATCH or DELETE]!' % (type)) raise ('Method not supported [%s] not in [GET, POST, PUT, PATCH or DELETE]!' % (type))
req.get_method = lambda: type.upper() req.get_method = lambda: type.upper()
request = urllib2.urlopen(req) request = urllib2.urlopen(req)
status = request.getcode()
if request.getcode() == 204: #No content returned, (ex: POST calendar/event/clear) if int(status) in (204, 404): # Page not found, no response
res = True response = False
elif request.getcode() == 404: #Page not found
res = False
else: else:
content=request.read() content = request.read()
res = simplejson.loads(content) response = simplejson.loads(content)
except urllib2.HTTPError,e:
if context.get('ask_time'):
try:
date = datetime.strptime(request.headers.get('date'), "%a, %d %b %Y %H:%M:%S %Z")
except:
date = datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
context['ask_time'] = date
except urllib2.HTTPError, e:
if e.code in (400, 401, 410):
raise e
_logger.exception("Bad google request : %s !" % e.read()) _logger.exception("Bad google request : %s !" % e.read())
raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong with your request to google"), context=context) raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong with your request to google"), context=context)
return res return (status, response)
def get_base_url(self, cr, uid, context=None): def get_base_url(self, cr, uid, context=None):
return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url',default='http://www.openerp.com?NoBaseUrl',context=context) return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='http://www.openerp.com?NoBaseUrl', context=context)
def get_client_id(self, cr, uid, service, context=None): def get_client_id(self, cr, uid, service, context=None):
return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_id' % (service,),default=False,context=context) return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_id' % (service,), default=False, context=context)
def get_client_secret(self, cr, uid, service, context=None): def get_client_secret(self, cr, uid, service, context=None):
return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_secret' % (service,),default=False,context=context) return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_secret' % (service,), default=False, context=context)
def get_uri_oauth(self,a=''): #a = optional action def get_uri_oauth(self, a=''): # a = optional action
return "https://accounts.google.com/o/oauth2/%s" % (a,) return "https://accounts.google.com/o/oauth2/%s" % (a,)
def get_uri_api(self): def get_uri_api(self):

View File

@ -25,20 +25,20 @@
'version': '1.0', 'version': '1.0',
'category': 'Tools', 'category': 'Tools',
'description': """ 'description': """
The module adds the possibility to synchronize Google Calendar with OpenERP The module adds the possibility to synchronize Google Calendar with OpenERP
======================================== ========================================
""", """,
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
'depends': ['google_account','calendar'], 'depends': ['google_account', 'calendar'],
'qweb': ['static/src/xml/*.xml'], 'qweb': ['static/src/xml/*.xml'],
'data': [ 'data': [
'res_config_view.xml', 'res_config_view.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/google_calendar.xml', 'views/google_calendar.xml',
'views/res_users.xml',
], ],
'demo': [], 'demo': [],
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,
} }

View File

@ -1,51 +1,60 @@
import simplejson
import urllib
import openerp
import openerp.addons.web.http as http import openerp.addons.web.http as http
from openerp.addons.web.http import request from openerp.addons.web.http import request
import openerp.addons.web.controllers.main as webmain
from openerp.addons.web.http import SessionExpiredException
from werkzeug.exceptions import BadRequest
import werkzeug.utils
class google_calendar_controller(http.Controller): class google_calendar_controller(http.Controller):
@http.route('/google_calendar/sync_data', type='json', auth='user') @http.route('/google_calendar/sync_data', type='json', auth='user')
def sync_data(self, arch, fields, model,**kw): def sync_data(self, arch, fields, model, **kw):
""" """
This route/function is called when we want to synchronize openERP calendar with Google Calendar This route/function is called when we want to synchronize openERP calendar with Google Calendar
Function return a dictionary with the status : need_config_from_admin, need_auth, need_refresh, success if not calendar_event Function return a dictionary with the status : need_config_from_admin, need_auth, need_refresh, success if not calendar_event
The dictionary may contains an url, to allow OpenERP Client to redirect user on this URL for authorization for example The dictionary may contains an url, to allow OpenERP Client to redirect user on this URL for authorization for example
""" """
if model == 'calendar.event': if model == 'calendar.event':
gs_obj = request.registry['google.service'] gs_obj = request.registry['google.service']
gc_obj = request.registry['google.calendar'] gc_obj = request.registry['google.calendar']
# Checking that admin have already configured Google API for google synchronization ! # Checking that admin have already configured Google API for google synchronization !
client_id = gs_obj.get_client_id(request.cr, request.uid,'calendar',context=kw.get('local_context')) client_id = gs_obj.get_client_id(request.cr, request.uid, 'calendar', context=kw.get('local_context'))
if not client_id or client_id == '': if not client_id or client_id == '':
action = '' action = ''
if gc_obj.can_authorize_google(request.cr,request.uid): if gc_obj.can_authorize_google(request.cr, request.uid):
dummy, action = request.registry.get('ir.model.data').get_object_reference(request.cr, request.uid, 'google_calendar', 'action_config_settings_google_calendar') dummy, action = request.registry.get('ir.model.data').get_object_reference(request.cr, request.uid,
'google_calendar', 'action_config_settings_google_calendar')
return { return {
"status" : "need_config_from_admin", "status": "need_config_from_admin",
"url" : '', "url": '',
"action" : action "action": action
} }
# Checking that user have already accepted OpenERP to access his calendar ! # Checking that user have already accepted OpenERP to access his calendar !
if gc_obj.need_authorize(request.cr, request.uid,context=kw.get('local_context')): if gc_obj.need_authorize(request.cr, request.uid, context=kw.get('local_context')):
url = gc_obj.authorize_google_uri(request.cr, request.uid, from_url=kw.get('fromurl'), context=kw.get('local_context')) url = gc_obj.authorize_google_uri(request.cr, request.uid, from_url=kw.get('fromurl'), context=kw.get('local_context'))
return { return {
"status" : "need_auth", "status": "need_auth",
"url" : url "url": url
} }
# If App authorized, and user access accepted, We launch the synchronization # If App authorized, and user access accepted, We launch the synchronization
return gc_obj.synchronize_events(request.cr, request.uid, [], kw.get('local_context')) return gc_obj.synchronize_events(request.cr, request.uid, [], context=kw.get('local_context'))
return { "status" : "success" } return {"status": "success"}
@http.route('/google_calendar/remove_references', type='json', auth='user')
def remove_references(self, model, **kw):
"""
This route/function is called when we want to remove all the references between one calendar OpenERP and one Google Calendar
"""
status = "NOP"
if model == 'calendar.event':
gc_obj = request.registry['google.calendar']
# Checking that user have already accepted OpenERP to access his calendar !
if gc_obj.remove_references(request.cr, request.uid, context=kw.get('local_context')):
status = "OK"
else:
status = "KO"
return {"status": status}

View File

@ -1,31 +1,15 @@
############################################################################## # -*- coding: utf-8 -*-
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2012 OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import operator import operator
import simplejson import simplejson
import urllib import urllib2
import openerp
from openerp import tools from openerp import tools
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
from openerp.http import request
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil import parser from dateutil import parser
import pytz import pytz
@ -35,6 +19,13 @@ import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def status_response(status, substr=False):
if substr:
return int(str(status)[0])
else:
return status_response(status, substr=True) == 2
class Meta(type): class Meta(type):
""" This Meta class allow to define class as a structure, and so instancied variable """ This Meta class allow to define class as a structure, and so instancied variable
in __init__ to avoid to have side effect alike 'static' variable """ in __init__ to avoid to have side effect alike 'static' variable """
@ -90,7 +81,7 @@ class SyncEvent(object):
def __getitem__(self, key): def __getitem__(self, key):
return getattr(self, key) return getattr(self, key)
def compute_OP(self): def compute_OP(self, modeFull=True):
#If event are already in Gmail and in OpenERP #If event are already in Gmail and in OpenERP
if self.OE.found and self.GG.found: if self.OE.found and self.GG.found:
#If the event has been deleted from one side, we delete on other side ! #If the event has been deleted from one side, we delete on other side !
@ -120,7 +111,6 @@ class SyncEvent(object):
else: else:
if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]: if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]:
self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar') self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar')
#import ipdb; ipdb.set_trace();
else: else:
self.OP = NothingToDo("", 'Not update needed') self.OP = NothingToDo("", 'Not update needed')
else: else:
@ -128,11 +118,13 @@ class SyncEvent(object):
# New in openERP... Create on create_events of synchronize function # New in openERP... Create on create_events of synchronize function
elif self.OE.found and not self.GG.found: elif self.OE.found and not self.GG.found:
#Has been deleted from gmail
if self.OE.status: if self.OE.status:
self.OP = Delete('OE', 'Removed from GOOGLE') self.OP = Delete('OE', 'Update or delete from GOOGLE')
else: else:
self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP") if not modeFull:
self.OP = Delete('GG', 'Deleted from OpenERP, need to delete it from Gmail if already created')
else:
self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP")
elif self.GG.found and not self.OE.found: elif self.GG.found and not self.OE.found:
tmpSrc = 'GG' tmpSrc = 'GG'
if not self.GG.status and not self.GG.isInstance: if not self.GG.status and not self.GG.isInstance:
@ -151,11 +143,11 @@ class SyncEvent(object):
return self.__repr__() return self.__repr__()
def __repr__(self): def __repr__(self):
myPrint = "---- A SYNC EVENT ---" myPrint = "\n\n---- A SYNC EVENT ---"
myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id) myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id)
myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False)) myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False))
myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name) myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name.encode('utf8'))
myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False)) myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', '').encode('utf8'))
myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found) myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found)
myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence) myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence)
myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance) myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance)
@ -207,15 +199,15 @@ class google_calendar(osv.AbstractModel):
STR_SERVICE = 'calendar' STR_SERVICE = 'calendar'
_name = 'google.%s' % STR_SERVICE _name = 'google.%s' % STR_SERVICE
def generate_data(self, cr, uid, event, context=None): def generate_data(self, cr, uid, event, isCreating=False, context=None):
if event.allday: if event.allday:
start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0] start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0]
end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0] final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration) + timedelta(days=isCreating and 1 or 0), context=context).isoformat('T').split('T')[0]
type = 'date' type = 'date'
vstype = 'dateTime' vstype = 'dateTime'
else: else:
start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T')
end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T')
type = 'dateTime' type = 'dateTime'
vstype = 'date' vstype = 'date'
attendee_list = [] attendee_list = []
@ -235,7 +227,7 @@ class google_calendar(osv.AbstractModel):
'timeZone': 'UTC' 'timeZone': 'UTC'
}, },
"end": { "end": {
type: end_date, type: final_date,
vstype: None, vstype: None,
'timeZone': 'UTC' 'timeZone': 'UTC'
}, },
@ -256,10 +248,9 @@ class google_calendar(osv.AbstractModel):
def create_an_event(self, cr, uid, event, context=None): def create_an_event(self, cr, uid, event, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid, event, isCreating=True, context=context)
data = self.generate_data(cr, uid, event, context=context) url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib2.quote('id,updated'), self.get_token(cr, uid, context))
url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib.quote('id,updated'), self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data_json = simplejson.dumps(data) data_json = simplejson.dumps(data)
@ -276,38 +267,89 @@ class google_calendar(osv.AbstractModel):
return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context) return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context)
def get_event_dict(self, cr, uid, token=False, nextPageToken=False, context=None): def get_calendar_primary_id(self, cr, uid, context=None):
params = {
'fields': 'id',
'access_token': self.get_token(cr, uid, context)
}
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
url = "/calendar/v3/calendars/primary"
try:
st, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context)
except Exception, e:
if (e.code == 401): # Token invalid / Acces unauthorized
error_msg = "Your token is invalid or has been revoked !"
registry = openerp.modules.registry.RegistryManager.get(request.session.db)
with registry.cursor() as cur:
self.pool['res.users'].write(cur, uid, [uid], {'google_calendar_token': False, 'google_calendar_token_validity': False}, context=context)
raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
raise
return status_response(st) and content['id'] or False
def get_event_synchro_dict(self, cr, uid, lastSync=False, token=False, nextPageToken=False, context=None):
if not token: if not token:
token = self.get_token(cr, uid, context) token = self.get_token(cr, uid, context)
gs_pool = self.pool['google.service']
params = { params = {
'fields': 'items,nextPageToken', 'fields': 'items,nextPageToken',
'access_token': token, 'access_token': token,
'maxResults': 1000, 'maxResults': 1000,
'timeMin': self.get_start_time_to_synchro(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"), #'timeMin': self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"),
} }
if lastSync:
params['updatedMin'] = lastSync.strftime("%Y-%m-%dT%H:%M:%S.%fz")
params['showDeleted'] = True
else:
params['timeMin'] = self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz")
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
url = "/calendar/v3/calendars/%s/events" % 'primary' url = "/calendar/v3/calendars/%s/events" % 'primary'
if nextPageToken: if nextPageToken:
params['pageToken'] = nextPageToken params['pageToken'] = nextPageToken
content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) status, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context)
google_events_dict = {} google_events_dict = {}
for google_event in content['items']: for google_event in content['items']:
google_events_dict[google_event['id']] = google_event google_events_dict[google_event['id']] = google_event
if content.get('nextPageToken', False): if content.get('nextPageToken'):
google_events_dict.update(self.get_event_dict(cr, uid, token, content['nextPageToken'], context=context)) google_events_dict.update(
self.get_event_synchro_dict(cr, uid, lastSync=lastSync, token=token, nextPageToken=content['nextPageToken'], context=context)
)
return google_events_dict return google_events_dict
def get_one_event_synchro(self, cr, uid, google_id, context=None):
token = self.get_token(cr, uid, context)
params = {
'access_token': token,
'maxResults': 1000,
'showDeleted': True,
}
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', google_id)
try:
status, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context)
except:
_logger.info("Calendar Synchro - In except of get_one_event_synchro")
pass
return status_response(status) and content or False
def update_to_google(self, cr, uid, oe_event, google_event, context): def update_to_google(self, cr, uid, oe_event, google_event, context):
calendar_event = self.pool['calendar.event'] calendar_event = self.pool['calendar.event']
gs_pool = self.pool['google.service']
url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context)) url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
@ -315,7 +357,7 @@ class google_calendar(osv.AbstractModel):
data['sequence'] = google_event.get('sequence', 0) data['sequence'] = google_event.get('sequence', 0)
data_json = simplejson.dumps(data) data_json = simplejson.dumps(data)
content = gs_pool._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context) status, content = self.pool['google.service']._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context)
update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz") update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date}) calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date})
@ -324,15 +366,13 @@ class google_calendar(osv.AbstractModel):
self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context) self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context)
def update_an_event(self, cr, uid, event, context=None): def update_an_event(self, cr, uid, event, context=None):
gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid, event, context=context) data = self.generate_data(cr, uid, event, context=context)
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id) url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id)
headers = {} headers = {}
data['access_token'] = self.get_token(cr, uid, context) data['access_token'] = self.get_token(cr, uid, context)
response = gs_pool._do_request(cr, uid, url, data, headers, type='GET', context=context) status, response = self.pool['google.service']._do_request(cr, uid, url, data, headers, type='GET', context=context)
#TO_CHECK : , if http fail, no event, do DELETE ? #TO_CHECK : , if http fail, no event, do DELETE ?
return response return response
@ -379,7 +419,12 @@ class google_calendar(osv.AbstractModel):
if self.get_need_synchro_attendee(cr, uid, context=context): if self.get_need_synchro_attendee(cr, uid, context=context):
attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context) attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context)
if not attendee_id: if not attendee_id:
attendee_id = [res_partner_obj.create(cr, uid, {'email': google_attendee['email'], 'customer': False, 'name': google_attendee.get("displayName", False) or google_attendee['email']}, context=context)] data = {
'email': google_attendee['email'],
'customer': False,
'name': google_attendee.get("displayName", False) or google_attendee['email']
}
attendee_id = [res_partner_obj.create(cr, uid, data, context=context)]
attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context)
partner_record.append((4, attendee.get('id'))) partner_record.append((4, attendee.get('id')))
attendee['partner_id'] = attendee.pop('id') attendee['partner_id'] = attendee.pop('id')
@ -388,26 +433,25 @@ class google_calendar(osv.AbstractModel):
UTC = pytz.timezone('UTC') UTC = pytz.timezone('UTC')
if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled
if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False): if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False):
date = parser.parse(single_event_dict['start']['dateTime']) date = parser.parse(single_event_dict['start']['dateTime'])
date_deadline = parser.parse(single_event_dict['end']['dateTime']) stop = parser.parse(single_event_dict['end']['dateTime'])
delta = date_deadline.astimezone(UTC) - date.astimezone(UTC)
date = str(date.astimezone(UTC))[:-6] date = str(date.astimezone(UTC))[:-6]
date_deadline = str(date_deadline.astimezone(UTC))[:-6] stop = str(stop.astimezone(UTC))[:-6]
allday = False allday = False
else: else:
date = (single_event_dict['start']['date'] + ' 00:00:00') date = (single_event_dict['start']['date'])
date_deadline = (single_event_dict['end']['date'] + ' 00:00:00') stop = (single_event_dict['end']['date'])
d_start = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") d_end = datetime.strptime(stop, DEFAULT_SERVER_DATE_FORMAT)
d_end = datetime.strptime(date_deadline, "%Y-%m-%d %H:%M:%S")
delta = (d_end - d_start)
allday = True allday = True
d_end = d_end + timedelta(days=-1)
stop = d_end.strftime(DEFAULT_SERVER_DATE_FORMAT)
result['duration'] = (delta.seconds / 60) / 60.0 + delta.days * 24
update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz") update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
result.update({ result.update({
'date': date, 'start': date,
'date_deadline': date_deadline, 'stop': stop,
'allday': allday 'allday': allday
}) })
result.update({ result.update({
@ -419,7 +463,6 @@ class google_calendar(osv.AbstractModel):
'location': single_event_dict.get('location', False), 'location': single_event_dict.get('location', False),
'class': single_event_dict.get('visibility', 'public'), 'class': single_event_dict.get('visibility', 'public'),
'oe_update_date': update_date, 'oe_update_date': update_date,
# 'google_internal_event_id': single_event_dict.get('id',False),
}) })
if single_event_dict.get("recurrence", False): if single_event_dict.get("recurrence", False):
@ -431,7 +474,6 @@ class google_calendar(osv.AbstractModel):
elif type == "copy": elif type == "copy":
result['recurrence'] = True result['recurrence'] = True
res = calendar_event.write(cr, uid, [event['id']], result, context=context) res = calendar_event.write(cr, uid, [event['id']], result, context=context)
elif type == "create": elif type == "create":
res = calendar_event.create(cr, uid, result, context=context) res = calendar_event.create(cr, uid, result, context=context)
@ -439,18 +481,81 @@ class google_calendar(osv.AbstractModel):
self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}, context) self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}, context)
return res return res
def synchronize_events(self, cr, uid, ids, context=None): def remove_references(self, cr, uid, context=None):
# Create all new events from OpenERP into Gmail, if that is not recurrent event current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
self.create_new_events(cr, uid, context=context) reset_data = {
self.bind_recurring_events_to_google(cr, uid, context) 'google_calendar_rtoken': False,
res = self.update_events(cr, uid, context) 'google_calendar_token': False,
'google_calendar_token_validity': False,
'google_calendar_last_sync_date': False,
'google_calendar_cal_id': False,
}
all_my_attendees = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', current_user.partner_id.id)], context=context)
self.pool['calendar.attendee'].write(cr, uid, all_my_attendees, {'oe_synchro_date': False, 'google_internal_event_id': False}, context=context)
current_user.write(reset_data, context=context)
return True
def synchronize_events(self, cr, uid, ids, lastSync=True, context=None):
if context is None:
context = {}
# def isValidSync(syncToken):
# gs_pool = self.pool['google.service']
# params = {
# 'maxResults': 1,
# 'fields': 'id',
# 'access_token': self.get_token(cr, uid, context),
# 'syncToken': syncToken,
# }
# url = "/calendar/v3/calendars/primary/events"
# status, response = gs_pool._do_request(cr, uid, url, params, type='GET', context=context)
# return int(status) != 410
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
context_with_time = dict(context.copy(), ask_time=True)
current_google = self.get_calendar_primary_id(cr, uid, context=context_with_time)
if current_user.google_calendar_cal_id:
if current_google != current_user.google_calendar_cal_id:
return {
"status": "need_reset",
"info": {
"old_name": current_user.google_calendar_cal_id,
"new_name": current_google
},
"url": ''
}
if lastSync and self.get_last_sync_date(cr, uid, context=context) and not self.get_disable_since_synchro(cr, uid, context=context):
lastSync = self.get_last_sync_date(cr, uid, context)
_logger.info("Calendar Synchro - MODE SINCE_MODIFIED : %s !" % lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
else:
lastSync = False
_logger.info("Calendar Synchro - MODE FULL SYNCHRO FORCED")
else:
current_user.write({'google_calendar_cal_id': current_google}, context=context)
lastSync = False
_logger.info("Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID")
new_ids = []
new_ids += self.create_new_events(cr, uid, context=context)
new_ids += self.bind_recurring_events_to_google(cr, uid, context)
res = self.update_events(cr, uid, lastSync, context)
current_user.write({'google_calendar_last_sync_date': context_with_time.get('ask_time')}, context=context)
return { return {
"status": res and "need_refresh" or "no_new_event_form_google", "status": res and "need_refresh" or "no_new_event_form_google",
"url": '' "url": ''
} }
def create_new_events(self, cr, uid, context=None): def create_new_events(self, cr, uid, context=None):
if context is None:
context = {}
new_ids = []
ev_obj = self.pool['calendar.event'] ev_obj = self.pool['calendar.event']
att_obj = self.pool['calendar.attendee'] att_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users'] user_obj = self.pool['res.users']
@ -458,32 +563,43 @@ class google_calendar(osv.AbstractModel):
context_norecurrent = context.copy() context_norecurrent = context.copy()
context_norecurrent['virtual_id'] = False context_norecurrent['virtual_id'] = False
my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
('google_internal_event_id', '=', False), ('google_internal_event_id', '=', False),
'|', '|',
('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")), ('event_id.stop', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")), ('event_id.final_date', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
], context=context_norecurrent) ], context=context_norecurrent)
for att in att_obj.browse(cr, uid, my_att_ids, context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0:
response = self.create_an_event(cr, uid, att.event_id, context=context) st, response = self.create_an_event(cr, uid, att.event_id, context=context)
update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz") if status_response(st):
ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date}) update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date}) ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date})
cr.commit() new_ids.append(response['id'])
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date})
cr.commit()
else:
_logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st))
_logger.warning("Response : %s" % response)
return new_ids
def bind_recurring_events_to_google(self, cr, uid, context): def get_context_no_virtual(self, context):
context_norecurrent = context.copy()
context_norecurrent['virtual_id'] = False
context_norecurrent['active_test'] = False
return context_norecurrent
def bind_recurring_events_to_google(self, cr, uid, context=None):
if context is None:
context = {}
new_ids = []
ev_obj = self.pool['calendar.event'] ev_obj = self.pool['calendar.event']
att_obj = self.pool['calendar.attendee'] att_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users'] user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
context_norecurrent = context.copy() context_norecurrent = self.get_context_no_virtual(context)
context_norecurrent['virtual_id'] = False
context_norecurrent['active_test'] = False
my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent) my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent)
for att in att_obj.browse(cr, uid, my_att_ids, context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
@ -500,11 +616,20 @@ class google_calendar(osv.AbstractModel):
if new_google_internal_event_id: if new_google_internal_event_id:
#TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE !
self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context) try:
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context) st, response = self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context)
cr.commit() if status_response(st):
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context)
new_ids.append(new_google_internal_event_id)
cr.commit()
else:
_logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st))
_logger.warning("Response : %s" % response)
except:
pass
return new_ids
def update_events(self, cr, uid, context=None): def update_events(self, cr, uid, lastSync=False, context=None):
if context is None: if context is None:
context = {} context = {}
@ -512,20 +637,64 @@ class google_calendar(osv.AbstractModel):
user_obj = self.pool['res.users'] user_obj = self.pool['res.users']
att_obj = self.pool['calendar.attendee'] att_obj = self.pool['calendar.attendee']
myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
context_novirtual = self.get_context_no_virtual(context)
context_novirtual = context.copy() if lastSync:
context_novirtual['virtual_id'] = False try:
context_novirtual['active_test'] = False all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=lastSync, context=context)
except urllib2.HTTPError, e:
if e.code == 410: # GONE, Google is lost.
# we need to force the rollback from this cursor, because it locks my res_users but I need to write in this tuple before to raise.
cr.rollback()
registry = openerp.modules.registry.RegistryManager.get(request.session.db)
with registry.cursor() as cur:
self.pool['res.users'].write(cur, uid, [uid], {'google_calendar_last_sync_date': False}, context=context)
error_key = simplejson.loads(e.read())
error_key = error_key.get('error', {}).get('message', 'nc')
error_msg = "Google are lost... the next synchro will be a full synchro. \n\n %s" % error_key
raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
all_event_from_google = self.get_event_dict(cr, uid, context=context) my_google_att_ids = att_obj.search(cr, uid, [
('partner_id', '=', myPartnerID),
('google_internal_event_id', 'in', all_event_from_google.keys())
], context=context_novirtual)
my_openerp_att_ids = att_obj.search(cr, uid, [
('partner_id', '=', myPartnerID),
('event_id.oe_update_date', '>', lastSync and lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
('google_internal_event_id', '!=', False),
], context=context_novirtual)
my_openerp_googleinternal_ids = att_obj.read(cr, uid, my_openerp_att_ids, ['google_internal_event_id', 'event_id'], context=context_novirtual)
if self.get_print_log(cr, uid, context=context):
_logger.info("Calendar Synchro - \n\nUPDATE IN GOOGLE\n%s\n\nRETRIEVE FROM OE\n%s\n\nUPDATE IN OE\n%s\n\nRETRIEVE FROM GG\n%s\n\n" % (all_event_from_google, my_google_att_ids, my_openerp_att_ids, my_openerp_googleinternal_ids))
for giid in my_openerp_googleinternal_ids:
active = True # if not sure, we request google
if giid.get('event_id'):
active = calendar_event.browse(cr, uid, int(giid.get('event_id')[0]), context=context_novirtual).active
if giid.get('google_internal_event_id') and not all_event_from_google.get(giid.get('google_internal_event_id')) and active:
one_event = self.get_one_event_synchro(cr, uid, giid.get('google_internal_event_id'), context=context)
if one_event:
all_event_from_google[one_event['id']] = one_event
my_att_ids = list(set(my_google_att_ids + my_openerp_att_ids))
else:
domain = [
('partner_id', '=', myPartnerID),
('google_internal_event_id', '!=', False),
'|',
('event_id.stop', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
('event_id.final_date', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
]
# Select all events from OpenERP which have been already synchronized in gmail
my_att_ids = att_obj.search(cr, uid, domain, context=context_novirtual)
all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=False, context=context)
# Select all events from OpenERP which have been already synchronized in gmail
my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
('google_internal_event_id', '!=', False),
'|',
('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_novirtual)
event_to_synchronize = {} event_to_synchronize = {}
for att in att_obj.browse(cr, uid, my_att_ids, context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
event = att.event_id event = att.event_id
@ -576,8 +745,10 @@ class google_calendar(osv.AbstractModel):
###################### ######################
for base_event in event_to_synchronize: for base_event in event_to_synchronize:
for current_event in event_to_synchronize[base_event]: for current_event in event_to_synchronize[base_event]:
event_to_synchronize[base_event][current_event].compute_OP() event_to_synchronize[base_event][current_event].compute_OP(modeFull=not lastSync)
#print event_to_synchronize[base_event] if self.get_print_log(cr, uid, context=context):
if not isinstance(event_to_synchronize[base_event][current_event].OP, NothingToDo):
_logger.info(event_to_synchronize[base_event])
###################### ######################
# DO ACTION # # DO ACTION #
@ -615,24 +786,35 @@ class google_calendar(osv.AbstractModel):
if actSrc == 'OE': if actSrc == 'OE':
self.delete_an_event(cr, uid, current_event[0], context=context) self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'GG': elif actSrc == 'GG':
new_google_event_id = event.GG.event['id'].split('_')[1] new_google_event_id = event.GG.event['id'].split('_')[1]
if 'T' in new_google_event_id: if 'T' in new_google_event_id:
new_google_event_id = new_google_event_id.replace('T', '')[:-1] new_google_event_id = new_google_event_id.replace('T', '')[:-1]
else: else:
new_google_event_id = new_google_event_id + "000000" new_google_event_id = new_google_event_id + "000000"
if event.GG.status: if event.GG.status:
parent_event = {} parent_event = {}
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) if not event_to_synchronize[base_event][0][1].OE.event_id:
res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) event_to_synchronize[base_event][0][1].OE.event_id = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].split('_')[0])], ['event_id'], context=context_novirtual)[0].get('event_id')[0]
else:
if event_to_synchronize[base_event][0][1].OE.event_id: parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)
calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), can_be_deleted=True, context=context) else:
parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), can_be_deleted=True, context=context)
elif isinstance(actToDo, Delete): elif isinstance(actToDo, Delete):
if actSrc == 'GG': if actSrc == 'GG':
self.delete_an_event(cr, uid, current_event[0], context=context) try:
self.delete_an_event(cr, uid, current_event[0], context=context)
except Exception, e:
error = simplejson.loads(e.read())
error_nr = error.get('error', {}).get('code')
# if already deleted from gmail or never created
if error_nr in (404, 410,):
pass
else:
raise e
elif actSrc == 'OE': elif actSrc == 'OE':
calendar_event.unlink(cr, uid, event.OE.event_id, can_be_deleted=False, context=context) calendar_event.unlink(cr, uid, event.OE.event_id, can_be_deleted=False, context=context)
return True return True
@ -655,7 +837,7 @@ class google_calendar(osv.AbstractModel):
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id) url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id)
content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) st, content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context)
return content.get('sequence', 0) return content.get('sequence', 0)
################################# #################################
## MANAGE CONNEXION TO GMAIL ## ## MANAGE CONNEXION TO GMAIL ##
@ -663,13 +845,16 @@ class google_calendar(osv.AbstractModel):
def get_token(self, cr, uid, context=None): def get_token(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
if not current_user.google_calendar_token_validity or \
if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)): datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) < (datetime.now() + timedelta(minutes=1)):
self.do_refresh_token(cr, uid, context=context) self.do_refresh_token(cr, uid, context=context)
current_user.refresh() current_user.refresh()
return current_user.google_calendar_token return current_user.google_calendar_token
def get_last_sync_date(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
return current_user.google_calendar_last_sync_date and datetime.strptime(current_user.google_calendar_last_sync_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(minutes=0) or False
def do_refresh_token(self, cr, uid, context=None): def do_refresh_token(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
@ -707,14 +892,18 @@ class google_calendar(osv.AbstractModel):
vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context) self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
def get_start_time_to_synchro(self, cr, uid, context=None): def get_minTime(self, cr, uid, context=None):
# WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 number_of_week = self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.week_synchro', default=13)
number_of_week = 13
return datetime.now() - timedelta(weeks=number_of_week) return datetime.now() - timedelta(weeks=number_of_week)
def get_need_synchro_attendee(self, cr, uid, context=None): def get_need_synchro_attendee(self, cr, uid, context=None):
# WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_synchro_attendee', default=True)
return True
def get_disable_since_synchro(self, cr, uid, context=None):
return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_since_synchro', default=False)
def get_print_log(self, cr, uid, context=None):
return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.debug_print', default=False)
class res_users(osv.Model): class res_users(osv.Model):
@ -724,16 +913,22 @@ class res_users(osv.Model):
'google_calendar_rtoken': fields.char('Refresh Token'), 'google_calendar_rtoken': fields.char('Refresh Token'),
'google_calendar_token': fields.char('User token'), 'google_calendar_token': fields.char('User token'),
'google_calendar_token_validity': fields.datetime('Token Validity'), 'google_calendar_token_validity': fields.datetime('Token Validity'),
'google_calendar_last_sync_date': fields.datetime('Last synchro date'),
'google_calendar_cal_id': fields.char('Calendar ID', help='Last Calendar ID who has been synchronized. If it is changed, we remove \
all links between GoogleID and OpenERP Google Internal ID')
} }
class calendar_event(osv.Model): class calendar_event(osv.Model):
_inherit = "calendar.event" _inherit = "calendar.event"
def get_fields_need_update_google(self, cr, uid, context=None):
return ['name', 'description', 'allday', 'date', 'date_end', 'stop', 'attendee_ids', 'location', 'class', 'active']
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):
if context is None: if context is None:
context = {} context = {}
sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class']) sync_fields = set(self.get_fields_need_update_google(cr, uid, context))
if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context: if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context:
vals['oe_update_date'] = datetime.now() vals['oe_update_date'] = datetime.now()
@ -762,7 +957,7 @@ class calendar_attendee(osv.Model):
_inherit = 'calendar.attendee' _inherit = 'calendar.attendee'
_columns = { _columns = {
'google_internal_event_id': fields.char('Google Calendar Event Id', size=256), 'google_internal_event_id': fields.char('Google Calendar Event Id'),
'oe_synchro_date': fields.datetime('OpenERP Synchro Date'), 'oe_synchro_date': fields.datetime('OpenERP Synchro Date'),
} }
_sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')] _sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]

View File

@ -11,38 +11,54 @@ openerp.google_calendar = function(instance) {
}); });
return this._super(r); return this._super(r);
}, },
sync_calendar: function(res,button) { sync_calendar: function(res, button) {
var self = this; var self = this;
var context = instance.web.pyeval.eval('context'); var context = instance.web.pyeval.eval('context');
//$('div.oe_cal_sync_button').hide(); //$('div.oe_cal_sync_button').hide();
$('div.oe_cal_sync_button').prop('disabled',true); $('div.oe_cal_sync_button').prop('disabled', true);
self.rpc('/google_calendar/sync_data', { self.rpc('/google_calendar/sync_data', {
arch: res.arch, arch: res.arch,
fields: res.fields, fields: res.fields,
model:res.model, model: res.model,
fromurl: window.location.href, fromurl: window.location.href,
local_context:context local_context: context
}).done(function(o) { }).done(function(o) {
if (o.status == "need_auth") { if (o.status === "need_auth") {
alert(_t("You will be redirected on gmail to authorize your OpenErp to access your calendar !")); alert(_t("You will be redirected on gmail to authorize your OpenErp to access your calendar !"));
instance.web.redirect(o.url); instance.web.redirect(o.url);
} }
else if (o.status == "need_config_from_admin") { else if (o.status === "need_config_from_admin"){
if (!_.isUndefined(o.action) && parseInt(o.action)){
if (!_.isUndefined(o.action) && parseInt(o.action)) { if (confirm(_t("An admin need to configure Google Synchronization before to use it, do you want to configure it now ? !"))){
if (confirm(_t("An admin need to configure Google Synchronization before to use it, do you want to configure it now ? !"))) { self.do_action(o.action);
self.do_action(o.action);
} }
} }
else { else{
alert(_t("An admin need to configure Google Synchronization before to use it !")); alert(_t("An admin need to configure Google Synchronization before to use it !"));
} }
} }
else if (o.status == "need_refresh"){ else if (o.status === "need_refresh"){
self.$calendar.fullCalendar('refetchEvents'); self.$calendar.fullCalendar('refetchEvents');
} }
else if (o.status === "need_reset"){
if (confirm(_t("The account that you are trying to synchronize (" + o.info.new_name + "), is not the same that the last one used \
(" + o.info.old_name + "! )" + "\r\n\r\nDo you want remove all references from the old account ?"))){
self.rpc('/google_calendar/remove_references', {
model:res.model,
local_context:context
}).done(function(o) {
if (o.status === "OK") {
alert(_t("All old references have been deleted. You can now restart the synchronization"));
}
else if (o.status === "KO") {
alert(_t("An error has occured when we was removing all old references. Please retry or contact your administrator."));
}
//else NOP
});
}
}
}).always(function(o) { $('div.oe_cal_sync_button').prop('disabled',false); }); }).always(function(o) { $('div.oe_cal_sync_button').prop('disabled',false); });
} }
}); });

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_users_form" model="ir.ui.view">
<field name="name">res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<notebook colspan="4" position="inside">
<page string="Calendar">
<group>
<field name="google_calendar_rtoken"/>
<field name="google_calendar_token"/>
<field name="google_calendar_token_validity"/>
<field name="google_calendar_last_sync_date"/>
<field name="google_calendar_cal_id"/>
</group>
</page>
</notebook>
</field>
</record>
</data>
</openerp>