GCal Mapping: Callback mechanism using channels
This commit is contained in:
parent
ba0cde09c1
commit
fcb04058b5
|
@ -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}
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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" ),
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
|
@ -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'
|
||||
|
|
|
@ -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() )
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 ),
|
||||
)
|
||||
|
|
@ -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("/")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in New Issue