blechreiz-website/eventplanner_gcal/models.py

321 lines
11 KiB
Python
Raw Normal View History

from apiclient.channel import new_webhook_channel, Channel
2014-03-08 22:36:25 +01:00
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
2014-03-08 22:36:25 +01:00
logger = logging.getLogger(__name__)
class GCalMapping( models.Model ):
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
"""
id = models.CharField( max_length=128, primary_key=True )
address = models.CharField( max_length=256 )
token = models.CharField( max_length=128 )
resource_id = models.CharField( max_length=128 )
expiration = models.IntegerField()
def toGChannel( self ):
return Channel( 'web_hook', self.id, self.token, self.address, self.expiration, resource_id = self.resource_id )
@staticmethod
def fromGChannel( gChannel ):
return GCalPushChannel( id = gChannel.id,
address = gChannel.address,
token = gChannel.token,
expiration = gChannel.expiration,
resource_id= gChannel.resource_id )
@staticmethod
def createNew( callbackUrl, service, ttl = None ):
gChannel = Channel('web_hook', str(uuid.uuid4()), 'blechreizGcal', callbackUrl, params= { 'ttl' : int(ttl) } )
response = service.events().watch( calendarId='primary', body= gChannel.body() ).execute()
gChannel.update( response )
dbChannel = GCalPushChannel.fromGChannel( gChannel )
dbChannel.save()
@staticmethod
def stop( service, gChannel ):
channelService = service.channels()
channelService.stop( body = gChannel.body() ).execute()
GCalPushChannel.fromGChannel( gChannel ).delete()
2014-03-08 22:36:25 +01:00
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 )
2014-03-08 22:36:25 +01:00
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" ):
2014-03-08 22:36:25 +01:00
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 {
2014-03-08 22:36:25 +01:00
'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 ),
}
# -------------------------------------------------------------------------------
2014-03-08 22:36:25 +01:00
2014-04-18 13:43:02 +02:00
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'
2014-03-08 22:36:25 +01:00
events = service.events().list(
calendarId='primary',
singleEvents=True,
maxResults=1000,
orderBy='startTime',
2014-04-18 13:43:02 +02:00
timeMin=minTime,
2014-03-08 22:36:25 +01:00
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
2014-03-08 22:36:25 +01:00
def syncGCalEvents():
if service is None:
logger.error("syncGCalEvents: Google API connection not configured")
return
allEvents = getAllGCalEvents()
2014-03-08 22:36:25 +01:00
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()
2014-03-08 22:36:25 +01:00
batch = BatchHttpRequest()
batchIsEmpty = True
for eventDjangoID in eventsToCreate_djangoID:
batch.add( createGCalEvent( Event.objects.get( pk=eventDjangoID ) ), callback=onGcalEventCreated )
2014-03-08 22:36:25 +01:00
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 )
2014-03-08 22:36:25 +01:00
2014-04-18 13:43:02 +02:00
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() )