From ee37a5ddcb6809dcd2ce018a6bac972a1a04edf5 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sat, 8 Mar 2014 22:36:25 +0100 Subject: [PATCH] Started with google calendar coupling --- blechreiz/settings.py | 10 + blechreiz/urls.py | 3 + eventplanner_gcal/__init__.py | 1 + .../googleapiTest/calendarCredentials.dat | 1 + .../management/commands/__init__.py | 1 + .../management/commands/events_gcal_sync.py | 10 + eventplanner_gcal/models.py | 171 ++++++++++++++++++ eventplanner_gcal/views.py | 8 + 8 files changed, 205 insertions(+) create mode 100644 eventplanner_gcal/__init__.py create mode 100644 eventplanner_gcal/googleapiTest/calendarCredentials.dat create mode 100644 eventplanner_gcal/management/commands/__init__.py create mode 100644 eventplanner_gcal/management/commands/events_gcal_sync.py create mode 100644 eventplanner_gcal/models.py create mode 100644 eventplanner_gcal/views.py diff --git a/blechreiz/settings.py b/blechreiz/settings.py index 49c03d7..1998979 100644 --- a/blechreiz/settings.py +++ b/blechreiz/settings.py @@ -158,6 +158,7 @@ INSTALLED_APPS = ( 'website', # Blechreiz Website in general 'musicians', # User Management 'eventplanner', # Event Management + 'eventplanner_gcal', # Event Management Sync with Google Calendar 'simpleforum', # Messages ( Forum ) 'location_field', # custom location field used in Event Management 'scoremanager', # manager of scores, repertoire etc. @@ -176,6 +177,15 @@ REST_FRAMEWORK = { } +GCAL_COUPLING = { + 'eventPrefix' : 'Blechreiz: ', + 'developerKey' : 'blechreiz-homepage', + 'clientId' : '34462582242-4kpdvvbi27ajt4u22uitqurpve9o8ipj.apps.googleusercontent.com', + 'client_secret' : 'y4t9XBrJdCODPTO5UvtONWWn', + 'credentials_file' : PROJECT_PATH + '/calendarCredentials.dat', +} + + CRISPY_TEMPLATE_PACK = 'bootstrap' # A sample logging configuration. The only tangible logging diff --git a/blechreiz/urls.py b/blechreiz/urls.py index 9640e25..bb14405 100644 --- a/blechreiz/urls.py +++ b/blechreiz/urls.py @@ -10,6 +10,8 @@ import musicians.urls import website.urls import scoremanager.urls +from eventplanner_gcal.views import * + import settings from django.conf.urls.static import static @@ -24,4 +26,5 @@ urlpatterns = patterns('', url(r'^admin/', include(admin.site.urls) ), url(r'^location_field/', include('location_field.urls')), #url(r'^gallery/', include(imagestore.urls, namespace='imagestore') ), + url(r'^eventSync/', runSync ) ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/eventplanner_gcal/__init__.py b/eventplanner_gcal/__init__.py new file mode 100644 index 0000000..a7501ae --- /dev/null +++ b/eventplanner_gcal/__init__.py @@ -0,0 +1 @@ +__author__ = 'martin' diff --git a/eventplanner_gcal/googleapiTest/calendarCredentials.dat b/eventplanner_gcal/googleapiTest/calendarCredentials.dat new file mode 100644 index 0000000..f3fd339 --- /dev/null +++ b/eventplanner_gcal/googleapiTest/calendarCredentials.dat @@ -0,0 +1 @@ +{"_module": "oauth2client.client", "token_expiry": "2014-03-08T14:33:53Z", "access_token": "ya29.1.AADtN_UH8eRhIXsdwF4W1D5oC8xYlMhG-0qeT210pyb6pxk7LTLmd-EnUNVS2UjJZQ", "token_uri": "https://accounts.google.com/o/oauth2/token", "invalid": false, "token_response": {"access_token": "ya29.1.AADtN_UH8eRhIXsdwF4W1D5oC8xYlMhG-0qeT210pyb6pxk7LTLmd-EnUNVS2UjJZQ", "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/eventplanner_gcal/management/commands/__init__.py b/eventplanner_gcal/management/commands/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/eventplanner_gcal/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/eventplanner_gcal/management/commands/events_gcal_sync.py b/eventplanner_gcal/management/commands/events_gcal_sync.py new file mode 100644 index 0000000..a810ad0 --- /dev/null +++ b/eventplanner_gcal/management/commands/events_gcal_sync.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand + +from eventplanner_gcal.models import syncGCalEvents + +class Command(BaseCommand): + help = 'Synchronize Google Calendar with locally stored Events' + + def handle(self, **options): + print ( "Running Sync") + syncGCalEvents() diff --git a/eventplanner_gcal/models.py b/eventplanner_gcal/models.py new file mode 100644 index 0000000..423e42c --- /dev/null +++ b/eventplanner_gcal/models.py @@ -0,0 +1,171 @@ +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 + +logger = logging.getLogger(__name__) + + + + +class GCalMapping( models.Model ): + gcal_id = models.CharField( max_length=64 ) + event = models.OneToOneField( Event, primary_key=True ) + + + + + +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 createEvent( 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 ) + + googleEvent = { + '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 ), + } + return service.events().insert(calendarId='primary', body=googleEvent) + + +def getAllEvents(pageToken=None): + events = service.events().list( + calendarId='primary', + singleEvents=True, + maxResults=1000, + orderBy='startTime', + timeMin='2000-01-01T00:00:00-00:00', + timeMax='2100-01-01T00:00:00-00:00', + pageToken=pageToken, + privateExtendedProperty='blechreizEvent=true', + ).execute() + return events['items'] + + +def onGcalEventCreated( request_id, response, exception ): + 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() + + +def syncGCalEvents(): + if service is None: + logger.error("syncGCalEvents: Google API connection not configured") + return + + allEvents = getAllEvents() + + 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 + + + batch = BatchHttpRequest() + + batchIsEmpty = True + for eventDjangoID in eventsToCreate_djangoID: + batch.add( createEvent( 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() + + + + + diff --git a/eventplanner_gcal/views.py b/eventplanner_gcal/views.py new file mode 100644 index 0000000..d61ba5f --- /dev/null +++ b/eventplanner_gcal/views.py @@ -0,0 +1,8 @@ +from django.shortcuts import redirect + +from eventplanner_gcal.models import syncGCalEvents + +def runSync( request ): + syncGCalEvents() + return redirect("/") +