Refactored google calendar sync
This commit is contained in:
parent
46b4078fac
commit
1aa23d2691
|
@ -1 +1 @@
|
|||
{"_module": "oauth2client.client", "token_expiry": "2014-04-20T19:09:26Z", "access_token": "ya29.1.AADtN_VE0fTw3cu2E9SR0XoblR3gXjnON1u2Qy__EB7E6HyEpaDky0LA-SmZVyok6A", "token_uri": "https://accounts.google.com/o/oauth2/token", "invalid": false, "token_response": {"access_token": "ya29.1.AADtN_VE0fTw3cu2E9SR0XoblR3gXjnON1u2Qy__EB7E6HyEpaDky0LA-SmZVyok6A", "token_type": "Bearer", "expires_in": 3600}, "client_id": "34462582242-4kpdvvbi27ajt4u22uitqurpve9o8ipj.apps.googleusercontent.com", "id_token": null, "client_secret": "y4t9XBrJdCODPTO5UvtONWWn", "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", "_class": "OAuth2Credentials", "refresh_token": "1/7-6-m_lLAKX8IeD7OuGtkcIiprty_nZUSxhMunSC5b0", "user_agent": null}
|
||||
{"_module": "oauth2client.client", "token_expiry": "2014-04-26T10:33:34Z", "access_token": "ya29.1.AADtN_WzqSiT0Ir0jy8f_InaX_NAUAs98E5YwU_uHYMMCvhfc90boeQSVPQpJQTWHw", "token_uri": "https://accounts.google.com/o/oauth2/token", "invalid": false, "token_response": {"access_token": "ya29.1.AADtN_WzqSiT0Ir0jy8f_InaX_NAUAs98E5YwU_uHYMMCvhfc90boeQSVPQpJQTWHw", "token_type": "Bearer", "expires_in": 3600}, "client_id": "34462582242-4kpdvvbi27ajt4u22uitqurpve9o8ipj.apps.googleusercontent.com", "id_token": null, "client_secret": "y4t9XBrJdCODPTO5UvtONWWn", "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", "_class": "OAuth2Credentials", "refresh_token": "1/7-6-m_lLAKX8IeD7OuGtkcIiprty_nZUSxhMunSC5b0", "user_agent": null}
|
|
@ -11,7 +11,7 @@ DEBUG = True
|
|||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@example.com'),
|
||||
('Martin Bauer', 'bauer_martin@gmx.de'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
@ -203,7 +203,7 @@ LOGGING = {
|
|||
'file': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/srv/test/eventplanner.log',
|
||||
'filename': PROJECT_PATH + '/eventplanner.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
|
|
|
@ -133,9 +133,9 @@ class EventParticipation( models.Model ):
|
|||
return self.user.username
|
||||
|
||||
def save( self, *args, **kwargs ):
|
||||
prev = EventParticipation.objects.get( event = self.event, user = self.user )
|
||||
if prev.status != self.status or prev.comment != self.comment:
|
||||
super(EventParticipation,self).save( *args,**kwargs)
|
||||
prev = EventParticipation.objects.get( event = self.event, user = self.user )
|
||||
if prev.status != self.status or prev.comment != self.comment:
|
||||
super(EventParticipation,self).save( *args,**kwargs)
|
||||
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -0,0 +1,330 @@
|
|||
import logging
|
||||
import httplib2
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from eventplanner.models import Event, EventParticipation
|
||||
from eventplanner_gcal.models import GCalMapping, GCalPushChannel
|
||||
from apiclient.http import BatchHttpRequest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------- Authentication using oauth2 --------------------------------------------
|
||||
|
||||
def createGCalServiceObject():
|
||||
"""Creates a Google API service object. This object is required whenever a Google API call is made"""
|
||||
from oauth2client.file import Storage
|
||||
from apiclient.discovery import build
|
||||
|
||||
gcal_settings = settings.GCAL_COUPLING
|
||||
|
||||
storage = Storage( gcal_settings['credentials_file'] )
|
||||
credentials = storage.get()
|
||||
|
||||
if credentials is None or credentials.invalid == True:
|
||||
logger.error("Unable to initialize Google Calendar coupling. Check your settings!")
|
||||
return None
|
||||
|
||||
http = httplib2.Http()
|
||||
http = credentials.authorize( http )
|
||||
res = build( serviceName='calendar', version='v3',
|
||||
http=http, developerKey=gcal_settings['developerKey'] )
|
||||
|
||||
if res is None:
|
||||
logger.error( "Authentication at google API failed. Check your settings!" )
|
||||
return res
|
||||
|
||||
|
||||
def getServiceObject():
|
||||
if getServiceObject.__serviceObject is None:
|
||||
getServiceObject.__serviceObject = createGCalServiceObject()
|
||||
|
||||
return getServiceObject.__serviceObject
|
||||
getServiceObject.__serviceObject = None
|
||||
|
||||
|
||||
|
||||
# --------------------- Building GCal event representation ------------------------------------
|
||||
|
||||
|
||||
def buildGCalAttendeesObj( event ):
|
||||
"""Builds a attendees object that is inserted into the GCal event.
|
||||
Attendees are all users that have a google mail address. """
|
||||
result = []
|
||||
for u in User.objects.all():
|
||||
if u.email.endswith( "@gmail.com") or u.email.endswith("@googlemail.com"):
|
||||
participation = EventParticipation.get_or_create( u, event )
|
||||
status = "tentative"
|
||||
if participation.status == 'Yes': status = "accepted"
|
||||
if participation.status == 'No' : status = "declined"
|
||||
|
||||
o = {
|
||||
'id': u.email,
|
||||
'email': u.email,
|
||||
'displayName': u.username,
|
||||
'comment': participation.comment,
|
||||
'responseStatus': status,
|
||||
}
|
||||
result.append( o )
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def buildGCalEvent( event, timezone="Europe/Berlin" ):
|
||||
""" Builds a GCal event using a local event. """
|
||||
|
||||
def createDateTimeObj( date, time ):
|
||||
if time is None:
|
||||
return { 'date': unicode(date), 'timeZone': timezone }
|
||||
else:
|
||||
return { 'dateTime': unicode(date) + 'T' + unicode(time) , 'timeZone': timezone }
|
||||
|
||||
startDate = event.date
|
||||
endDate = event.end_date
|
||||
if endDate is None: endDate = startDate
|
||||
|
||||
startTime = event.meeting_time
|
||||
if startTime is None: startTime = event.time
|
||||
|
||||
if startTime is None:
|
||||
endTime = None
|
||||
else:
|
||||
endTime = datetime.time( 22, 30 )
|
||||
|
||||
return {
|
||||
'summary': unicode(settings.GCAL_COUPLING['eventPrefix'] + event.title),
|
||||
'description': unicode(event.desc),
|
||||
'location': unicode(event.location),
|
||||
'start': createDateTimeObj( startDate, startTime ),
|
||||
'end' : createDateTimeObj( endDate, endTime ),
|
||||
'extendedProperties': {
|
||||
'private': {
|
||||
'blechreizEvent': 'true',
|
||||
'blechreizID': event.id,
|
||||
}
|
||||
},
|
||||
'attendees': buildGCalAttendeesObj( event ),
|
||||
}
|
||||
|
||||
|
||||
|
||||
# ------------------------------ GCal Api Calls -------------------------------------------------
|
||||
|
||||
def getAllGCalEvents( service, fromNow=False ):
|
||||
"""Retrieves all gcal events with custom property blechreizEvent=True i.e. all
|
||||
events that have been created by this script."""
|
||||
|
||||
if fromNow:
|
||||
now = datetime.datetime.now()
|
||||
minTime = now.strftime("%Y-%m-%dT%H:%M:%S-00:00")
|
||||
else:
|
||||
minTime = '2000-01-01T00:00:00-00:00'
|
||||
|
||||
events = service.events().list(
|
||||
calendarId='primary',
|
||||
singleEvents=True,
|
||||
maxResults=1000,
|
||||
orderBy='startTime',
|
||||
timeMin=minTime,
|
||||
timeMax='2100-01-01T00:00:00-00:00',
|
||||
privateExtendedProperty='blechreizEvent=true',
|
||||
).execute()
|
||||
return events['items']
|
||||
|
||||
def createGCalEvent( service, event, timezone="Europe/Berlin" ):
|
||||
"""Creates a new gcal event using a local event"""
|
||||
googleEvent = buildGCalEvent(event,timezone)
|
||||
return service.events().insert(calendarId='primary', body=googleEvent)
|
||||
|
||||
def updateGCalEvent( service, event, timezone="Europe/Berlin"):
|
||||
"""Updates an existing gcal event, using a local event"""
|
||||
googleEvent = buildGCalEvent(event,timezone)
|
||||
mapping = GCalMapping.objects.get( event=event )
|
||||
gcalId = mapping.gcal_id
|
||||
return service.events().patch(calendarId='primary', eventId= gcalId, body=googleEvent)
|
||||
|
||||
def deleteGCalEvent( service, event ):
|
||||
"""Deletes gcal that belongs to the given local event"""
|
||||
mapping = GCalMapping.objects.get( event=event )
|
||||
gcalId = mapping.gcal_id
|
||||
return service.events().delete(calendarId='primary', eventId=gcalId)
|
||||
|
||||
|
||||
|
||||
# ------------------------------------- Synchronization ----------------------------------------------------
|
||||
|
||||
def deleteAllGCalEvents( service = getServiceObject() ):
|
||||
"""Deletes all gcal events that have been created by this script"""
|
||||
|
||||
if service is None: service = getServiceObject()
|
||||
|
||||
gcalIds = [ ev['id'] for ev in getAllGCalEvents( service ) ]
|
||||
l = len( gcalIds )
|
||||
if l == 0:
|
||||
return l
|
||||
|
||||
batch = BatchHttpRequest()
|
||||
for id in gcalIds:
|
||||
batch.add( service.events().delete(calendarId='primary', eventId=id) )
|
||||
batch.execute()
|
||||
|
||||
return l
|
||||
|
||||
def syncFromLocalToGoogle( service = None ):
|
||||
""" Creates a google event for each local event (if it does not exist yet) and deletes all google events
|
||||
that are not found in local database. Updates participation info of gcal events using local data
|
||||
"""
|
||||
if service is None: service = getServiceObject()
|
||||
|
||||
allEvents = getAllGCalEvents( service )
|
||||
|
||||
eventsAtGoogle_djangoID = set()
|
||||
eventsAtGoogle_googleID = set()
|
||||
for gcalEv in allEvents:
|
||||
eventsAtGoogle_djangoID.add( int(gcalEv['extendedProperties']['private']['blechreizID'] ) )
|
||||
eventsAtGoogle_googleID.add( gcalEv['id'] )
|
||||
|
||||
localEvents_djangoID = set( Event. objects.all().values_list('pk' , flat=True) )
|
||||
localEvents_googleID = set( GCalMapping.objects.all().values_list('gcal_id', flat=True) )
|
||||
|
||||
eventsToCreate_djangoID = localEvents_djangoID - eventsAtGoogle_djangoID
|
||||
eventsToDelete_googleID = eventsAtGoogle_googleID - localEvents_googleID
|
||||
|
||||
def onGcalEventCreated( request_id, response, exception ):
|
||||
"""Callback function for created events to enter new gcal id in the mapping table"""
|
||||
if exception is not None:
|
||||
print ( "response " + str( response ) )
|
||||
raise exception
|
||||
|
||||
googleId = response['id']
|
||||
djangoId = response['extendedProperties']['private']['blechreizID']
|
||||
mapping = GCalMapping( gcal_id = googleId, event = Event.objects.get( pk=djangoId ) )
|
||||
mapping.save()
|
||||
|
||||
|
||||
|
||||
batch = BatchHttpRequest()
|
||||
|
||||
batchIsEmpty = True
|
||||
for eventDjangoID in eventsToCreate_djangoID:
|
||||
batch.add( createGCalEvent( service, Event.objects.get( pk=eventDjangoID ) ), callback=onGcalEventCreated )
|
||||
batchIsEmpty=False
|
||||
|
||||
for eventGoogleID in eventsToDelete_googleID:
|
||||
batch.add( service.events().delete(calendarId='primary', eventId=eventGoogleID) )
|
||||
batchIsEmpty=False
|
||||
|
||||
for gcalEv in allEvents:
|
||||
eventDjangoID = int( gcalEv['extendedProperties']['private']['blechreizID'] )
|
||||
try:
|
||||
djangoEv = Event.objects.get( pk=eventDjangoID )
|
||||
if gcalEv['attendees'] != buildGCalAttendeesObj( djangoEv ):
|
||||
batch.add( updateGCalEvent( service, djangoEv ) )
|
||||
batchIsEmpty = False
|
||||
except Event.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not batchIsEmpty:
|
||||
batch.execute()
|
||||
|
||||
|
||||
|
||||
return len (eventsToCreate_djangoID), len(eventsToDelete_googleID)
|
||||
|
||||
|
||||
|
||||
def syncFromGoogleToLocal( service = None ):
|
||||
"""Retrieves only participation infos for all events and updates local database if anything has changed. """
|
||||
|
||||
if service is None: service = getServiceObject()
|
||||
|
||||
allEvents = getAllGCalEvents( service, fromNow=True)
|
||||
for e in allEvents:
|
||||
localId = e['extendedProperties']['private']['blechreizID']
|
||||
localEvent = Event.objects.get( pk=localId )
|
||||
for a in e['attendees']:
|
||||
user = User.objects.get( email= a['email'] )
|
||||
part = EventParticipation.get_or_create( user, localEvent )
|
||||
if 'comment' in a:
|
||||
part.comment = a['comment']
|
||||
|
||||
if a['responseStatus'] == 'needsAction' or a['responseStatus']=='tentative':
|
||||
part.status = '?'
|
||||
elif a['responseStatus'] == 'accepted':
|
||||
part.status = 'Yes'
|
||||
elif a['responseStatus'] == 'declined':
|
||||
part.status = 'No'
|
||||
else:
|
||||
logger.error("Unknown response status when mapping gcal event: " + a['responseStatus'] )
|
||||
|
||||
prev = EventParticipation.objects.get( event = part.event, user = part.user )
|
||||
|
||||
# Important: Save only if the participation info has changed
|
||||
# otherwise everything is synced back to google via the post save signal
|
||||
# and an endless loop is entered
|
||||
if prev.status != part.status or prev.comment != part.comment:
|
||||
part.save()
|
||||
|
||||
|
||||
|
||||
# ------------------------------------- Synchronization ----------------------------------------------------
|
||||
|
||||
|
||||
def checkGCalSubscription( service=None, timeToLive = 14*24*3600, renewBeforeExpiry = None ):
|
||||
"""Google offers a push service if any event information has changed. This works using a so called
|
||||
channel, which has a certain time to live. This method checks that a valid channel exists:
|
||||
- if none exists a new one is created
|
||||
- if existing channel does expire soon, the channel is renewed
|
||||
- if channel has already expired a sync is triggered and a new channel is created
|
||||
"""
|
||||
if service is None: service = getServiceObject()
|
||||
|
||||
if renewBeforeExpiry is None:
|
||||
renewBeforeExpiry = 0.8 * timeToLive
|
||||
|
||||
callbackUrl = settings.GCAL_COUPLING['push_url']
|
||||
|
||||
# Test if a channel already exists for this callbackURL
|
||||
try:
|
||||
dbChannel = GCalPushChannel.objects.get( address=callbackUrl )
|
||||
gChannel = dbChannel.toGChannel()
|
||||
|
||||
# if expiration time between 0 and two days: stop and create new channel
|
||||
curTime = int( time.time() * 1000)
|
||||
if gChannel.expiration > curTime:
|
||||
# not yet expired
|
||||
if curTime + renewBeforeExpiry*1000 > gChannel.expiration:
|
||||
#will expire in less than "renewBeforeExpiry"
|
||||
print ( "Renewing Google Calendar Subscription: " + callbackUrl )
|
||||
GCalPushChannel.stop( service, gChannel )
|
||||
GCalPushChannel.createNew( callbackUrl, service, timeToLive )
|
||||
else:
|
||||
print ("Channel active until %d " % ( gChannel.expiration, ) )
|
||||
else:
|
||||
logger.info( "Google calendar subscription had expired - getting new subscription" )
|
||||
# to get back in sync again we have to decide which data to take
|
||||
# so we use the local data as reference
|
||||
syncFromLocalToGoogle( service )
|
||||
GCalPushChannel.createNew( callbackUrl, service, timeToLive )
|
||||
|
||||
except GCalPushChannel.DoesNotExist:
|
||||
# create new channel and save it in database
|
||||
logger.info ( "No CGalCallback Channel exists yet for: " + callbackUrl )
|
||||
# to get back in sync again we have to decide which data to take
|
||||
# so we use the local data as reference
|
||||
syncFromLocalToGoogle( service )
|
||||
GCalPushChannel.createNew( callbackUrl, service, timeToLive )
|
||||
|
||||
|
||||
def stopAllGCalSubscriptions( service=None ):
|
||||
"""Stops the channel subscription """
|
||||
|
||||
if service is None: service = getServiceObject()
|
||||
|
||||
for dbChannel in GCalPushChannel.objects.all():
|
||||
print("Stopping %s expiry at %d " % ( dbChannel.id, dbChannel.expiration ) )
|
||||
GCalPushChannel.stop( service, dbChannel.toGChannel() )
|
|
@ -1,6 +1,6 @@
|
|||
from django.core.management.base import NoArgsCommand
|
||||
|
||||
from eventplanner_gcal.signals import checkGCalSubscription
|
||||
from eventplanner_gcal.google_sync import checkGCalSubscription
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = 'Checks if the GCal notification channel is still active'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.core.management.base import NoArgsCommand
|
||||
from eventplanner_gcal.signals import deleteAllGCalEvents
|
||||
from eventplanner_gcal.google_sync import deleteAllGCalEvents
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = 'Delete all events in the google calendar created by this app'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.core.management.base import NoArgsCommand
|
||||
|
||||
from eventplanner_gcal.signals import stopAllGCalSubscriptions
|
||||
from eventplanner_gcal.google_sync import stopAllGCalSubscriptions
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = 'Stops all GCal subscriptions'
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from django.core.management.base import NoArgsCommand
|
||||
|
||||
from eventplanner_gcal.signals import syncGCalEvents
|
||||
from eventplanner_gcal.google_sync import syncFromLocalToGoogle
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = 'Synchronize Google Calendar with locally stored Events'
|
||||
def handle_noargs(self, **options):
|
||||
print ( "Running Sync")
|
||||
created, deleted = syncGCalEvents()
|
||||
created, deleted = syncFromLocalToGoogle()
|
||||
print ( "Created %d and deleted %d events" % (created,deleted) )
|
||||
|
|
|
@ -1,32 +1,23 @@
|
|||
from apiclient.channel import new_webhook_channel, Channel
|
||||
from django.db import models
|
||||
from eventplanner.models import Event, EventParticipation
|
||||
|
||||
from apiclient.discovery import build
|
||||
from apiclient.http import BatchHttpRequest
|
||||
|
||||
from oauth2client.file import Storage
|
||||
import httplib2
|
||||
|
||||
from django.conf import settings
|
||||
import logging
|
||||
import datetime
|
||||
from django.contrib.auth.models import User
|
||||
import time
|
||||
import uuid
|
||||
from eventplanner.models import Event
|
||||
|
||||
from apiclient.channel import Channel
|
||||
from django.db import models
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class GCalMapping( models.Model ):
|
||||
"""Mapping between event id at google and local event id"""
|
||||
gcal_id = models.CharField( max_length=64 )
|
||||
event = models.OneToOneField( Event, primary_key=True )
|
||||
|
||||
|
||||
class GCalPushChannel( models.Model ):
|
||||
"""This table has either zero or one entry. Required to store if a channel already exists,
|
||||
when it expires, and how to stop (renew) the channel
|
||||
when it expires and how to stop (renew) the channel
|
||||
"""
|
||||
|
||||
id = models.CharField( max_length=128, primary_key=True )
|
||||
|
@ -60,262 +51,3 @@ class GCalPushChannel( models.Model ):
|
|||
channelService = service.channels()
|
||||
channelService.stop( body = gChannel.body() ).execute()
|
||||
GCalPushChannel.fromGChannel( gChannel ).delete()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def init( gcal_settings ):
|
||||
"""Creates a API service object required by the following synchronization functions"""
|
||||
storage = Storage( gcal_settings['credentials_file'] )
|
||||
credentials = storage.get()
|
||||
|
||||
if credentials is None or credentials.invalid == True:
|
||||
logger.error("Unable to initialize Google Calendar coupling. Check your settings!")
|
||||
return None
|
||||
|
||||
http = httplib2.Http()
|
||||
http = credentials.authorize( http )
|
||||
return build( serviceName='calendar', version='v3',
|
||||
http=http, developerKey=gcal_settings['developerKey'] )
|
||||
|
||||
|
||||
|
||||
service = init( settings.GCAL_COUPLING )
|
||||
|
||||
|
||||
def createAttendeesObj( event ):
|
||||
result = []
|
||||
for u in User.objects.all():
|
||||
if u.email.endswith( "@gmail.com") or u.email.endswith("@googlemail.com"):
|
||||
participation = EventParticipation.get_or_create( u, event )
|
||||
status = "tentative"
|
||||
if participation.status == 'Yes': status = "accepted"
|
||||
if participation.status == 'No' : status = "declined"
|
||||
|
||||
o = {
|
||||
'id': u.email,
|
||||
'email': u.email,
|
||||
'displayName': u.username,
|
||||
'comment': participation.comment,
|
||||
'responseStatus': status,
|
||||
}
|
||||
result.append( o )
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def buildGCalEvent( event, timezone="Europe/Berlin" ):
|
||||
if service is None:
|
||||
logger.error("createEvent: Google API connection not configured")
|
||||
return
|
||||
|
||||
|
||||
def createDateTimeObj( date, time ):
|
||||
if time is None:
|
||||
return { 'date': unicode(date), 'timeZone': timezone }
|
||||
else:
|
||||
return { 'dateTime': unicode(date) + 'T' + unicode(time) , 'timeZone': timezone }
|
||||
|
||||
startDate = event.date
|
||||
endDate = event.end_date
|
||||
if endDate is None: endDate = startDate
|
||||
|
||||
startTime = event.meeting_time
|
||||
if startTime is None: startTime = event.time
|
||||
|
||||
if startTime is None:
|
||||
endTime = None
|
||||
else:
|
||||
endTime = datetime.time( 22, 30 )
|
||||
|
||||
return {
|
||||
'summary': unicode(settings.GCAL_COUPLING['eventPrefix'] + event.title),
|
||||
'description': unicode(event.desc),
|
||||
'location': unicode(event.location),
|
||||
'start': createDateTimeObj( startDate, startTime ),
|
||||
'end' : createDateTimeObj( endDate, endTime ),
|
||||
'extendedProperties': {
|
||||
'private': {
|
||||
'blechreizEvent': 'true',
|
||||
'blechreizID': event.id,
|
||||
}
|
||||
},
|
||||
'attendees': createAttendeesObj( event ),
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
def getAllGCalEvents( fromNow=False, pageToken=None ):
|
||||
|
||||
if fromNow:
|
||||
now = datetime.datetime.now()
|
||||
minTime = now.strftime("%Y-%m-%dT%H:%M:%S-00:00")
|
||||
else:
|
||||
minTime = '2000-01-01T00:00:00-00:00'
|
||||
|
||||
events = service.events().list(
|
||||
calendarId='primary',
|
||||
singleEvents=True,
|
||||
maxResults=1000,
|
||||
orderBy='startTime',
|
||||
timeMin=minTime,
|
||||
timeMax='2100-01-01T00:00:00-00:00',
|
||||
pageToken=pageToken,
|
||||
privateExtendedProperty='blechreizEvent=true',
|
||||
).execute()
|
||||
return events['items']
|
||||
|
||||
|
||||
def createGCalEvent( event, timezone="Europe/Berlin" ):
|
||||
googleEvent = buildGCalEvent(event,timezone)
|
||||
return service.events().insert(calendarId='primary', body=googleEvent)
|
||||
|
||||
def updateGCalEvent( event, timezone="Europe/Berlin"):
|
||||
googleEvent = buildGCalEvent(event,timezone)
|
||||
mapping = GCalMapping.objects.get( event=event )
|
||||
gcalId = mapping.gcal_id
|
||||
return service.events().patch(calendarId='primary', eventId= gcalId, body=googleEvent)
|
||||
|
||||
def deleteGCalEvent( event ):
|
||||
mapping = GCalMapping.objects.get( event=event )
|
||||
gcalId = mapping.gcal_id
|
||||
return service.events().delete(calendarId='primary', eventId=gcalId)
|
||||
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
def deleteAllGCalEvents():
|
||||
if service is None:
|
||||
logger.error("deleteAllGCalEvents: Google API connection not configured")
|
||||
return
|
||||
|
||||
gcalIds = [ ev['id'] for ev in getAllGCalEvents() ]
|
||||
l = len(gcalIds)
|
||||
if l == 0:
|
||||
return l
|
||||
|
||||
batch = BatchHttpRequest()
|
||||
for id in gcalIds:
|
||||
batch.add( service.events().delete(calendarId='primary', eventId=id) )
|
||||
batch.execute()
|
||||
|
||||
return l
|
||||
|
||||
|
||||
|
||||
|
||||
def syncGCalEvents():
|
||||
if service is None:
|
||||
logger.error("syncGCalEvents: Google API connection not configured")
|
||||
return
|
||||
|
||||
allEvents = getAllGCalEvents()
|
||||
|
||||
eventsAtGoogle_djangoID = set()
|
||||
eventsAtGoogle_googleID = set()
|
||||
for gcalEv in allEvents:
|
||||
eventsAtGoogle_djangoID.add( int(gcalEv['extendedProperties']['private']['blechreizID'] ) )
|
||||
eventsAtGoogle_googleID.add( gcalEv['id'] )
|
||||
|
||||
localEvents_djangoID = set( Event. objects.all().values_list('pk' , flat=True) )
|
||||
localEvents_googleID = set( GCalMapping.objects.all().values_list('gcal_id', flat=True) )
|
||||
|
||||
eventsToCreate_djangoID = localEvents_djangoID - eventsAtGoogle_djangoID
|
||||
eventsToDelete_googleID = eventsAtGoogle_googleID - localEvents_googleID
|
||||
|
||||
|
||||
def onGcalEventCreated( request_id, response, exception ):
|
||||
"""Callback function for created events"""
|
||||
if exception is not None:
|
||||
print ( "response " + str( response ) )
|
||||
raise exception
|
||||
|
||||
googleId = response['id']
|
||||
djangoId = response['extendedProperties']['private']['blechreizID']
|
||||
mapping = GCalMapping( gcal_id = googleId, event = Event.objects.get( pk=djangoId ) )
|
||||
mapping.save()
|
||||
|
||||
|
||||
batch = BatchHttpRequest()
|
||||
|
||||
batchIsEmpty = True
|
||||
for eventDjangoID in eventsToCreate_djangoID:
|
||||
batch.add( createGCalEvent( Event.objects.get( pk=eventDjangoID ) ), callback=onGcalEventCreated )
|
||||
batchIsEmpty=False
|
||||
|
||||
for eventGoogleID in eventsToDelete_googleID:
|
||||
batch.add( service.events().delete(calendarId='primary', eventId=eventGoogleID) )
|
||||
batchIsEmpty=False
|
||||
|
||||
if not batchIsEmpty:
|
||||
batch.execute()
|
||||
|
||||
return len (eventsToCreate_djangoID), len(eventsToDelete_googleID )
|
||||
|
||||
|
||||
def syncParticipationFromGoogleToLocal():
|
||||
allEvents = getAllGCalEvents(fromNow=True)
|
||||
for e in allEvents:
|
||||
localId = e['extendedProperties']['private']['blechreizID']
|
||||
localEvent = Event.objects.get( pk=localId )
|
||||
for a in e['attendees']:
|
||||
user = User.objects.get( email= a['email'] )
|
||||
part = EventParticipation.get_or_create( user, localEvent )
|
||||
if 'comment' in a:
|
||||
part.comment = a['comment']
|
||||
|
||||
if a['responseStatus'] == 'needsAction' or a['responseStatus']=='tentative':
|
||||
part.status = '?'
|
||||
elif a['responseStatus'] == 'accepted':
|
||||
part.status = 'Yes'
|
||||
elif a['responseStatus'] == 'declined':
|
||||
part.status = 'No'
|
||||
else:
|
||||
logger.error("Unknown response status when mapping gcal event: " + a['responseStatus'] )
|
||||
|
||||
part.save()
|
||||
|
||||
|
||||
def checkGCalSubscription():
|
||||
global service
|
||||
callbackUrl = settings.GCAL_COUPLING['push_url']
|
||||
|
||||
timeToLive = 14*24*3600 # how long the channel should be active
|
||||
renewBeforeExpiry = 2*24*3600 # duration before expiry when channel is renewed
|
||||
#timeToLive = 60*5
|
||||
#renewBeforeExpiry = 60*3
|
||||
|
||||
# Test if a channel already exists for this callbackURL
|
||||
try:
|
||||
dbChannel = GCalPushChannel.objects.get( address=callbackUrl )
|
||||
gChannel = dbChannel.toGChannel()
|
||||
|
||||
# if expiration time between 0 and two days: stop and create new channel
|
||||
curTime = int( time.time() * 1000)
|
||||
if gChannel.expiration > curTime:
|
||||
# not yet expired
|
||||
if curTime + renewBeforeExpiry*1000 > gChannel.expiration:
|
||||
#will expire in less than "renewBeforeExpiry"
|
||||
print ( "Renewing Google Calendar Subscription: " + callbackUrl )
|
||||
GCalPushChannel.stop( service, gChannel )
|
||||
GCalPushChannel.createNew( callbackUrl, service, timeToLive )
|
||||
else:
|
||||
print ("Channel active until %d " % ( gChannel.expiration, ) )
|
||||
else:
|
||||
print( "Google calendar subscription had expired - getting new subscription" )
|
||||
syncParticipationFromGoogleToLocal()
|
||||
GCalPushChannel.createNew( callbackUrl, service, timeToLive )
|
||||
|
||||
except GCalPushChannel.DoesNotExist:
|
||||
# create new channel and save it in database
|
||||
print ( "No CGalCallback Channel exists yet for: " + callbackUrl )
|
||||
syncParticipationFromGoogleToLocal()
|
||||
GCalPushChannel.createNew( callbackUrl, service, timeToLive )
|
||||
|
||||
|
||||
def stopAllGCalSubscriptions():
|
||||
for dbChannel in GCalPushChannel.objects.all():
|
||||
print("Stopping %s expiry at %d " % ( dbChannel.id, dbChannel.expiration ) )
|
||||
GCalPushChannel.stop( service, dbChannel.toGChannel() )
|
||||
|
|
|
@ -2,105 +2,41 @@ from django.db.models.signals import post_save,pre_delete
|
|||
from django.dispatch import receiver
|
||||
from eventplanner.models import Event, EventParticipation
|
||||
from django.contrib.auth.models import User
|
||||
import eventplanner_gcal.models
|
||||
from eventplanner_gcal.google_sync import getServiceObject, syncFromLocalToGoogle,\
|
||||
createGCalEvent, deleteGCalEvent, updateGCalEvent
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger( __name__ )
|
||||
|
||||
|
||||
class SignalLock:
|
||||
def __init__(self):
|
||||
self.locked=False
|
||||
|
||||
def __enter__(self):
|
||||
if self.locked:
|
||||
return False
|
||||
|
||||
self.locked=True
|
||||
return True
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.locked=False
|
||||
|
||||
def isLocked(self):
|
||||
return self.locked
|
||||
|
||||
signalLock = SignalLock()
|
||||
|
||||
|
||||
def onGoogleCallback():
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
logger.info( "Sync back from google" )
|
||||
eventplanner_gcal.models.syncParticipationFromGoogleToLocal()
|
||||
|
||||
|
||||
@receiver( post_save, sender=User )
|
||||
def user_changed( **kwargs ):
|
||||
logger.info("User info changed")
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
logger.info("Synchronizing with google - user information changed")
|
||||
eventplanner_gcal.models.deleteAllGCalEvents()
|
||||
eventplanner_gcal.models.syncGCalEvents()
|
||||
logger.info("Synchronizing with google - user information changed")
|
||||
syncFromLocalToGoogle( getServiceObject() )
|
||||
|
||||
|
||||
@receiver( post_save,sender= Event)
|
||||
def event_post_save_handler( **kwargs):
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
event = kwargs['instance']
|
||||
created = kwargs['created']
|
||||
if created:
|
||||
logger.info("Creating Gcal event")
|
||||
eventplanner_gcal.models.createGCalEvent( event ).execute()
|
||||
else:
|
||||
logger.info( "Updating Gcal event")
|
||||
eventplanner_gcal.models.updateGCalEvent( event ).execute()
|
||||
event = kwargs['instance']
|
||||
created = kwargs['created']
|
||||
if created:
|
||||
logger.info("Creating Gcal event")
|
||||
createGCalEvent( getServiceObject(), event ).execute()
|
||||
else:
|
||||
logger.info( "Updating Gcal event")
|
||||
updateGCalEvent( getServiceObject(),event ).execute()
|
||||
|
||||
|
||||
|
||||
@receiver( pre_delete,sender= Event)
|
||||
def event_pre_delete_handler( **kwargs):
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
event = kwargs['instance']
|
||||
logger.info ("Deleting GCAL event")
|
||||
eventplanner_gcal.models.deleteGCalEvent( event ).execute()
|
||||
event = kwargs['instance']
|
||||
logger.info ("Deleting GCAL event")
|
||||
deleteGCalEvent( getServiceObject(), event ).execute()
|
||||
|
||||
|
||||
@receiver( post_save, sender=EventParticipation )
|
||||
def participation_post_save_handler( **kwargs):
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
participation = kwargs['instance']
|
||||
logger.info("Participation post save -> update gcal")
|
||||
eventplanner_gcal.models.updateGCalEvent( participation.event ).execute()
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------- For management commands ------------------------
|
||||
|
||||
|
||||
def stopAllGCalSubscriptions():
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
eventplanner_gcal.models.stopAllGCalSubscriptions()
|
||||
|
||||
def checkGCalSubscription():
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
eventplanner_gcal.models.checkGCalSubscription()
|
||||
|
||||
|
||||
def deleteAllGCalEvents():
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
eventplanner_gcal.models.deleteAllGCalEvents()
|
||||
|
||||
def syncGCalEvents():
|
||||
if not signalLock.isLocked():
|
||||
with signalLock:
|
||||
eventplanner_gcal.models.syncGCalEvents()
|
||||
|
||||
participation = kwargs['instance']
|
||||
logger.info("Participation post save -> update gcal")
|
||||
updateGCalEvent( getServiceObject(), participation.event ).execute()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.shortcuts import redirect
|
||||
from eventplanner_gcal.models import syncGCalEvents, syncParticipationFromGoogleToLocal
|
||||
from eventplanner_gcal.signals import onGoogleCallback
|
||||
from eventplanner_gcal.google_sync import syncFromGoogleToLocal, syncFromLocalToGoogle
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
@ -10,13 +9,12 @@ logger = logging.getLogger( __name__ )
|
|||
|
||||
|
||||
def runSync( request ):
|
||||
syncGCalEvents()
|
||||
|
||||
syncFromLocalToGoogle()
|
||||
return redirect("/")
|
||||
|
||||
@csrf_exempt
|
||||
def gcalApiCallback( request ):
|
||||
onGoogleCallback()
|
||||
syncFromGoogleToLocal()
|
||||
logger.info("Received callback from GCal - updating event participations... ")
|
||||
return HttpResponse('<h1>Callback successful</h1>')
|
||||
|
||||
|
|
Loading…
Reference in New Issue