GCal Mapping: Callback mechanism using channels

This commit is contained in:
Martin Bauer 2014-04-19 20:36:01 +02:00 committed by Martin Bauer
parent ba0cde09c1
commit fcb04058b5
16 changed files with 185 additions and 45 deletions

View File

@ -0,0 +1 @@
{"_module": "oauth2client.client", "token_expiry": "2014-04-19T18:37:13Z", "access_token": "ya29.1.AADtN_UNi3N8wJ5AiTZ3ced1ho7-ZOhe9cV0TqJ7lByEy_h5SHGm0EuZBSWVOQ3q3g", "token_uri": "https://accounts.google.com/o/oauth2/token", "invalid": false, "token_response": {"access_token": "ya29.1.AADtN_UNi3N8wJ5AiTZ3ced1ho7-ZOhe9cV0TqJ7lByEy_h5SHGm0EuZBSWVOQ3q3g", "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}

View File

@ -183,6 +183,8 @@ GCAL_COUPLING = {
'clientId' : '34462582242-4kpdvvbi27ajt4u22uitqurpve9o8ipj.apps.googleusercontent.com',
'client_secret' : 'y4t9XBrJdCODPTO5UvtONWWn',
'credentials_file' : PROJECT_PATH + '/calendarCredentials.dat',
'push_url' : "https://test.bauer.technology/eventplanner_gcal/gcalApiCallback",
#'push_url' : "https://test.bauer.technology/callbackTest2.php",
}

View File

@ -9,8 +9,9 @@ import musicians.urls
#import imagestore.urls
import website.urls
import scoremanager.urls
import eventplanner_gcal.urls
from eventplanner_gcal.views import *
import settings
from django.conf.urls.static import static
@ -18,13 +19,13 @@ from django.conf.urls.static import static
admin.autodiscover()
urlpatterns = patterns('',
url(r'^', include(website.urls) ),
url(r'^', include( website.urls ) ),
url(r'^events/', include( eventplanner.urls.urlpatterns) ),
url(r'^musicians/', include( musicians.urls.urlpatterns) ),
url(r'^scores/', include( scoremanager.urls.urlpatterns) ),
url(r'^messages/$', simpleforum.views.message_view ),
url(r'^admin/', include(admin.site.urls) ),
url(r'^location_field/', include('location_field.urls')),
url(r'^admin/', include( admin.site.urls ) ),
url(r'^location_field/', include( 'location_field.urls' ) ),
url(r'^eventplanner_gcal/', include( eventplanner_gcal.urls) ),
#url(r'^gallery/', include(imagestore.urls, namespace='imagestore') ),
url(r'^eventSync/', runSync )
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -6,7 +6,7 @@
{% addtoblock "css" strip %} <link rel="stylesheet" href="{{STATIC_URL}}/css/bootstrap-overrides.css"> {% endaddtoblock %}
{% addtoblock "css" strip %} <link rel="stylesheet" href="{{STATIC_URL}}/css/theme.css" type="text/css"> {% endaddtoblock %}
{% addtoblock "css" strip %} <link rel="stylesheet" href="{{STATIC_URL}}/css/index.css" type="text/css" media="screen" /> {% endaddtoblock %}
{% addtoblock "css" strip %} <link href='http://fonts.googleapis.com/css?family=Lato:300,400,700,900,300italic,400italic,700italic,900italic' rel='stylesheet' type='text/css'>{% endaddtoblock %}
{% addtoblock "css" strip %} <link href='https://fonts.googleapis.com/css?family=Lato:300,400,700,900,300italic,400italic,700italic,900italic' rel='stylesheet' type='text/css'>{% endaddtoblock %}
{% addtoblock "css" %}
<!--[if lt IE 9]>

View File

@ -116,8 +116,6 @@ class Event ( models.Model ):
return nextEvent
before_read = Signal()
class EventParticipation( models.Model ):
OPTIONS = ( ('?' , _('?' )),
@ -147,10 +145,6 @@ class EventParticipation( models.Model ):
else:
return True
@staticmethod
def raiseBeforeReadSignal():
before_read.send( sender=EventParticipation )
@staticmethod
def isMember( user ):
return user.has_perm('eventplanner.member')

View File

@ -9,10 +9,10 @@ urlpatterns = patterns('',
url(r'^grid$', events_grid ),
url(r'^planning$', eventplanning ),
url(r'^(?P<pk>\d+)$', permission_required('eventplanner.change_event')( EventUpdate.as_view() ) ),
url(r'^add$', permission_required('eventplanner.add_event')( EventCreate.as_view() ) ),
url(r'^add$', permission_required('eventplanner.add_event' )( EventCreate.as_view() ) ),
url(r'^(?P<pk>\d+)/delete$', permission_required('eventplanner.delete_event')( deleteEvent ) ),
url(r'^api/', event_api, name="event_api" ),
url(r'^api/(\w+)/$', event_api, name="event_api_per_user" ),
url(r'^api/(\w+)/(\d+)$', event_api, name="event_api_per_user_event"),
url(r'^api/(\w+)/(\d+)$', event_api, name="event_api_per_user_event" ),
)

View File

@ -61,7 +61,6 @@ def eventplanning( request ):
"""
View for a specific user, to edit his events
"""
EventParticipation.raiseBeforeReadSignal()
# non-members see the grid - but cannot edit anything
if not EventParticipation.isMember( request.user ):
return events_grid(request)
@ -78,8 +77,6 @@ def eventplanning( request ):
def events_grid( request ):
EventParticipation.raiseBeforeReadSignal()
usernames = [ u.username for u in EventParticipation.members() ]
all_future_events = list ( Event.objects.filter( date__gte = datetime.date.today() ).order_by( 'date') )
@ -135,7 +132,6 @@ class EventUpdate( UpdateView ):
class EventCreate( CreateView ):
form_class = EventForm
model = Event
template_name_suffix = "_update_form"

View File

@ -0,0 +1,9 @@
from django.core.management.base import NoArgsCommand
from eventplanner_gcal.signals import checkGCalSubscription
class Command(NoArgsCommand):
help = 'Checks if the GCal notification channel is still active'
def handle_noargs(self, **options):
print ( "Checking Subscription")
checkGCalSubscription()

View File

@ -1,5 +1,5 @@
from django.core.management.base import NoArgsCommand
from eventplanner_gcal.models import deleteAllGCalEvents
from eventplanner_gcal.signals import deleteAllGCalEvents
class Command(NoArgsCommand):
help = 'Delete all events in the google calendar created by this app'

View File

@ -0,0 +1,8 @@
from django.core.management.base import NoArgsCommand
from eventplanner_gcal.signals import stopAllGCalSubscriptions
class Command(NoArgsCommand):
help = 'Stops all GCal subscriptions'
def handle_noargs(self, **options):
stopAllGCalSubscriptions()

View File

@ -1,6 +1,6 @@
from django.core.management.base import NoArgsCommand
from eventplanner_gcal.models import syncGCalEvents
from eventplanner_gcal.signals import syncGCalEvents
class Command(NoArgsCommand):
help = 'Synchronize Google Calendar with locally stored Events'

View File

@ -1,3 +1,4 @@
from apiclient.channel import new_webhook_channel, Channel
from django.db import models
from eventplanner.models import Event, EventParticipation
@ -11,17 +12,56 @@ from django.conf import settings
import logging
import datetime
from django.contrib.auth.models import User
import time
import uuid
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()
@ -40,8 +80,8 @@ def init( gcal_settings ):
http=http, developerKey=gcal_settings['developerKey'] )
service = init( settings.GCAL_COUPLING )
service = init( settings.GCAL_COUPLING )
def createAttendeesObj( event ):
@ -217,7 +257,6 @@ def syncGCalEvents():
def syncParticipationFromGoogleToLocal():
allEvents = getAllGCalEvents(fromNow=True)
for e in allEvents:
localId = e['extendedProperties']['private']['blechreizID']
localEvent = Event.objects.get( pk=localId )
@ -237,3 +276,46 @@ def syncParticipationFromGoogleToLocal():
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() )

View File

@ -1,10 +1,10 @@
from django.db.models.signals import post_save,pre_delete
from django.dispatch import receiver
from eventplanner.models import Event, EventParticipation, before_read
from eventplanner.models import Event, EventParticipation
from django.contrib.auth.models import User
from eventplanner_gcal.models import createGCalEvent, updateGCalEvent
from eventplanner_gcal.models import deleteGCalEvent, syncParticipationFromGoogleToLocal
import eventplanner_gcal.models
class SignalLock:
def __init__(self):
@ -25,6 +25,12 @@ class SignalLock:
signalLock = SignalLock()
@receiver( post_save, sender=User )
def user_changed( **kwargs ):
if not signalLock.isLocked():
with signalLock:
eventplanner_gcal.models.syncGCalEvents()
@receiver( post_save,sender= Event)
def event_post_save_handler( **kwargs):
@ -34,10 +40,10 @@ def event_post_save_handler( **kwargs):
created = kwargs['created']
if created:
print("Creating Gcal event")
createGCalEvent( event ).execute()
eventplanner_gcal.models.createGCalEvent( event ).execute()
else:
print( "Updating Gcal event")
updateGCalEvent( event ).execute()
eventplanner_gcal.models.updateGCalEvent( event ).execute()
@ -47,7 +53,7 @@ def event_pre_delete_handler( **kwargs):
with signalLock:
event = kwargs['instance']
print ("Deleting GCAL event")
deleteGCalEvent( event ).execute()
eventplanner_gcal.models.deleteGCalEvent( event ).execute()
@receiver( post_save, sender=EventParticipation )
@ -56,12 +62,32 @@ def participation_post_save_handler( **kwargs):
with signalLock:
participation = kwargs['instance']
print("Participation post save -> update gcal")
updateGCalEvent( participation.event ).execute()
eventplanner_gcal.models.updateGCalEvent( participation.event ).execute()
@receiver( before_read, sender=EventParticipation )
def participation_before_read_handler( **kwargs):
# -------------- For management commands ------------------------
def stopAllGCalSubscriptions():
if not signalLock.isLocked():
with signalLock:
print("SyncParticipation from google")
syncParticipationFromGoogleToLocal()
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()

View File

@ -0,0 +1,9 @@
from django.conf.urls import patterns, url
from views import runSync, gcalApiCallback
urlpatterns = patterns('',
url(r'^runSync$', runSync ),
url(r'^gcalApiCallback$', gcalApiCallback ),
)

View File

@ -1,8 +1,20 @@
from django.shortcuts import redirect
from eventplanner_gcal.models import syncGCalEvents, syncParticipationFromGoogleToLocal
from eventplanner_gcal.models import syncGCalEvents
def runSync( request ):
syncGCalEvents()
return redirect("/")
def gcalApiCallback( request ):
syncParticipationFromGoogleToLocal()
print ( "gcalApiCallback called" )
return redirect("/")

View File

@ -37,7 +37,7 @@
<li><a href="/events"> Termine</a></li>
<li><a href="/messages">Forum</a></li>
<li><a href="/musicians">Adressbuch</a></li>
<li><a href="/scores"> Noten</a></li>
<!-- <li><a href="/scores"> Noten</a></li> -->
{% endblock %}
{% if user.is_authenticated %}