diff --git a/blechreiz/calendarCredentials.dat b/blechreiz/calendarCredentials.dat index 1ae3261..32a5754 100644 --- a/blechreiz/calendarCredentials.dat +++ b/blechreiz/calendarCredentials.dat @@ -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} \ No newline at end of file +{"_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} \ No newline at end of file diff --git a/blechreiz/settings.py b/blechreiz/settings.py index 5b3f0fd..cfb4491 100644 --- a/blechreiz/settings.py +++ b/blechreiz/settings.py @@ -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': { diff --git a/eventplanner/models.py b/eventplanner/models.py index 7e52c0c..4b022e1 100644 --- a/eventplanner/models.py +++ b/eventplanner/models.py @@ -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 diff --git a/eventplanner_gcal/google_sync.py b/eventplanner_gcal/google_sync.py new file mode 100644 index 0000000..ae7bd0c --- /dev/null +++ b/eventplanner_gcal/google_sync.py @@ -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() ) diff --git a/eventplanner_gcal/management/commands/gcal_checkSubscription.py b/eventplanner_gcal/management/commands/gcal_checkSubscription.py index 02c7eed..69aa950 100644 --- a/eventplanner_gcal/management/commands/gcal_checkSubscription.py +++ b/eventplanner_gcal/management/commands/gcal_checkSubscription.py @@ -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' diff --git a/eventplanner_gcal/management/commands/gcal_delete_all.py b/eventplanner_gcal/management/commands/gcal_delete_all.py index 70e67b0..cfb2864 100644 --- a/eventplanner_gcal/management/commands/gcal_delete_all.py +++ b/eventplanner_gcal/management/commands/gcal_delete_all.py @@ -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' diff --git a/eventplanner_gcal/management/commands/gcal_stopSubscriptions.py b/eventplanner_gcal/management/commands/gcal_stopSubscriptions.py index e3d153a..ec44aeb 100644 --- a/eventplanner_gcal/management/commands/gcal_stopSubscriptions.py +++ b/eventplanner_gcal/management/commands/gcal_stopSubscriptions.py @@ -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' diff --git a/eventplanner_gcal/management/commands/gcal_sync.py b/eventplanner_gcal/management/commands/gcal_sync.py index 5a215a4..98e3983 100644 --- a/eventplanner_gcal/management/commands/gcal_sync.py +++ b/eventplanner_gcal/management/commands/gcal_sync.py @@ -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) ) diff --git a/eventplanner_gcal/models.py b/eventplanner_gcal/models.py index 29e12c2..b764ced 100644 --- a/eventplanner_gcal/models.py +++ b/eventplanner_gcal/models.py @@ -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() ) diff --git a/eventplanner_gcal/signals.py b/eventplanner_gcal/signals.py index 5059ebb..f2c319e 100644 --- a/eventplanner_gcal/signals.py +++ b/eventplanner_gcal/signals.py @@ -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() diff --git a/eventplanner_gcal/views.py b/eventplanner_gcal/views.py index 3dbe326..fead45c 100644 --- a/eventplanner_gcal/views.py +++ b/eventplanner_gcal/views.py @@ -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('

Callback successful

')