changeset 857:9165edfb1709

For issue #80, use new Google Calendar v3 API.
author Brian Neal <bgneal@gmail.com>
date Sat, 22 Nov 2014 13:27:13 -0600
parents c2dfd1b1323e
children a4d685cdecdd
files gcalendar/admin.py gcalendar/calendar.py gcalendar/models.py gcalendar/oauth.py sg101/templates/gcalendar/google_sync.html
diffstat 5 files changed, 117 insertions(+), 187 deletions(-) [+]
line wrap: on
line diff
--- a/gcalendar/admin.py	Thu Nov 20 18:47:54 2014 -0600
+++ b/gcalendar/admin.py	Sat Nov 22 13:27:13 2014 -0600
@@ -11,7 +11,7 @@
 from django.http import HttpResponseRedirect
 from django.shortcuts import render
 
-from gcalendar.models import Event, AccessToken
+from gcalendar.models import Event
 from gcalendar.calendar import Calendar, CalendarError
 from gcalendar import oauth
 
@@ -19,15 +19,15 @@
 
 
 class EventAdmin(admin.ModelAdmin):
-    list_display = ('what', 'user', 'start_date', 'where', 'date_submitted',
-            'status', 'is_approved', 'google_html')
-    list_filter = ('start_date', 'status')
+    list_display = ['what', 'user', 'start_date', 'where', 'date_submitted',
+            'status', 'is_approved', 'google_html']
+    list_filter = ['start_date', 'status']
     date_hierarchy = 'start_date'
-    search_fields = ('what', 'where', 'description')
-    raw_id_fields = ('user', )
-    exclude = ('html', 'google_id', 'google_url')
+    search_fields = ['what', 'where', 'description']
+    raw_id_fields = ['user']
+    exclude = ['html', 'google_id', 'google_url']
     save_on_top = True
-    actions = ('approve_events', )
+    actions = ['approve_events']
 
     pending_states = {
         Event.NEW: Event.NEW_APRV,
@@ -85,18 +85,21 @@
         msgs = []
         err_msg = ''
         if request.method == 'POST':
-            if access_token:
+            credentials = oauth.get_credentials()
+            if credentials:
                 try:
-                    cal = Calendar(source=oauth.USER_AGENT,
-                            calendar_id=settings.GCAL_CALENDAR_ID,
-                            access_token=access_token)
+                    cal = Calendar(calendar_id=settings.GCAL_CALENDAR_ID,
+                            credentials=credentials)
                     cal.sync_events(events)
                 except CalendarError, e:
-                    err_msg = e.msg
+                    err_msg = str(e)
                     events = Event.pending_events.all()
                 else:
                     msgs.append('All events processed successfully.')
                     events = Event.objects.none()
+            else:
+                self.message_user(request, "Invalid or missing credentials",
+                        level=messages.ERROR)
 
         return render(request, 'gcalendar/google_sync.html', {
             'current_app': self.admin_site.name,
@@ -113,7 +116,7 @@
         site = Site.objects.get_current()
         callback_url = 'http://%s%s' % (site.domain,
                 reverse('admin:gcalendar-auth_return'))
-        auth_url = oauth.get_auth_url(request, callback_url)
+        auth_url = oauth.get_auth_url(callback_url)
         return HttpResponseRedirect(auth_url)
 
     def auth_return(self, request):
@@ -131,4 +134,3 @@
 
 
 admin.site.register(Event, EventAdmin)
-admin.site.register(AccessToken)
--- a/gcalendar/calendar.py	Thu Nov 20 18:47:54 2014 -0600
+++ b/gcalendar/calendar.py	Sat Nov 22 13:27:13 2014 -0600
@@ -1,122 +1,120 @@
 """
-This file contains the calendar class wich abstracts the Google gdata API for working with
-Google Calendars.
+This file contains the calendar class wich abstracts the Google API for working
+with Google Calendars.
 
 """
 import datetime
 import pytz
 
+import httplib2
+from apiclient.discovery import build
+from apiclient.http import BatchHttpRequest
 from django.utils.tzinfo import FixedOffset
-from gdata.calendar.client import CalendarClient
-from gdata.calendar.data import (CalendarEventEntry, CalendarEventFeed,
-        CalendarWhere, When, EventWho)
-import atom.data
 
 from gcalendar.models import Event
 
 
 class CalendarError(Exception):
-    def __init__(self, msg):
-        self.msg = msg
+    """Calendar exception base class."""
 
-    def __str__(self):
-        return repr(self.msg)
+
+def _make_err_msg(event, exception):
+    """Returns an error message string from the given Event and exception."""
+    return '%s - %s' % (event.what, exception)
 
 
 class Calendar(object):
-    DATE_FMT = '%Y-%m-%d'
-    DATE_TIME_FMT = DATE_FMT + 'T%H:%M:%S'
-    DATE_TIME_TZ_FMT = DATE_TIME_FMT + '.000Z'
+    DATE_TIME_FMT = '%Y-%m-%dT%H:%M:%S'
+    DATE_TIME_TZ_FMT = DATE_TIME_FMT + 'Z'
 
-    def __init__(self, source=None, calendar_id='default', access_token=None):
-        self.client = CalendarClient(source=source, auth_token=access_token)
-
-        self.insert_feed = ('https://www.google.com/calendar/feeds/'
-            '%s/private/full' % calendar_id)
-        self.batch_feed = '%s/batch' % self.insert_feed
+    def __init__(self, calendar_id='primary', credentials=None):
+        self.calendar_id = calendar_id
+        http = httplib2.Http()
+        if credentials:
+            http = credentials.authorize(http)
+        self.service = build('calendar', 'v3', http=http)
 
     def sync_events(self, qs):
-        request_feed = CalendarEventFeed()
-        for model in qs:
-            if model.status == Event.NEW_APRV:
-                event = CalendarEventEntry()
-                request_feed.AddInsert(entry=self._populate_event(model, event))
-            elif model.status == Event.EDIT_APRV:
-                event = self._retrieve_event(model)
-                request_feed.AddUpdate(entry=self._populate_event(model, event))
-            elif model.status == Event.DEL_APRV:
-                event = self._retrieve_event(model)
-                request_feed.AddDelete(entry=event)
+        """Process the pending events in a batch request to the Google calendar
+        API.
+        """
+        batch = BatchHttpRequest()
+
+        err_msgs = []
+        def insert_callback(request_id, event, exception):
+            n = int(request_id)
+            if not exception:
+                qs[n].status = Event.ON_CAL
+                qs[n].google_id = event['id']
+                qs[n].google_url = event['htmlLink']
+                qs[n].save()
+                qs[n].notify_on_calendar()
             else:
-                assert False, 'unexpected status in sync_events'
+                err_msgs.append(_make_err_msg(qs[n], exception))
+
+        def update_callback(request_id, event, exception):
+            n = int(request_id)
+            if not exception:
+                qs[n].status = Event.ON_CAL
+                qs[n].save()
+            else:
+                err_msgs.append(_make_err_msg(qs[n], exception))
+
+        def delete_callback(request_id, response, exception):
+            n = int(request_id)
+            if not exception:
+                qs[n].delete()
+            else:
+                err_msgs.append(_make_err_msg(qs[n], exception))
+
+        for n, event in enumerate(qs):
+            if event.status == Event.NEW_APRV:
+                batch.add(self.service.events().insert(calendarId=self.calendar_id,
+                            body=self._make_event(event)),
+                        callback=insert_callback,
+                        request_id=str(n))
+            elif event.status == Event.EDIT_APRV:
+                batch.add(self.service.events().update(calendarId=self.calendar_id,
+                            eventId=event.google_id,
+                            body=self._make_event(event)),
+                        callback=update_callback,
+                        request_id=str(n))
+            elif event.status == Event.DEL_APRV:
+                batch.add(self.service.events().delete(calendarId=self.calendar_id,
+                            eventId=event.google_id),
+                        callback=delete_callback,
+                        request_id=str(n))
+            else:
+                raise CalendarError("Invalid event status: %s" % event.status)
 
         try:
-            response_feed = self.client.ExecuteBatch(request_feed, self.batch_feed)
-        except Exception, e:
-            raise CalendarError('ExecuteBatch exception: %s' % e)
+            batch.execute()
+        except Exception as ex:
+            raise CalendarError('Batch exception: %s' % ex)
 
-        err_msgs = []
-        for entry in response_feed.entry:
-            i = int(entry.batch_id.text)
-            code = int(entry.batch_status.code)
-
-            error = False
-            if qs[i].status == Event.NEW_APRV:
-                if code == 201:
-                    qs[i].status = Event.ON_CAL
-                    qs[i].google_id = entry.GetEditLink().href
-                    qs[i].google_url = entry.GetHtmlLink().href
-                    qs[i].save()
-                    qs[i].notify_on_calendar()
-                else:
-                    error = True
-
-            elif qs[i].status == Event.EDIT_APRV:
-                if code == 200:
-                    qs[i].status = Event.ON_CAL
-                    qs[i].save()
-                else:
-                    error = True
-
-            elif qs[i].status == Event.DEL_APRV:
-                if code == 200:
-                    qs[i].delete()
-                else:
-                    error = True
-
-            if error:
-                err_msgs.append('%s - (%d) %s' % (
-                    qs[i].what, code, entry.batch_status.reason))
-
-        if len(err_msgs) > 0:
+        if err_msgs:
             raise CalendarError(', '.join(err_msgs))
 
-    def _retrieve_event(self, model):
-        try:
-            event = self.client.GetEventEntry(model.google_id)
-        except Exception, e:
-            raise CalendarError('Could not retrieve event from Google: %s, %s' \
-                    % (model.what, e))
-        return event
-
-    def _populate_event(self, model, event):
-        """Populates a gdata event from an Event model object."""
-        event.title = atom.data.Title(text=model.what)
-        event.content = atom.data.Content(text=model.html)
-        event.where = [CalendarWhere(value=model.where)]
-        event.who = [EventWho(email=model.user.email)]
+    def _make_event(self, model):
+        """Creates an event body from a model instance."""
+        event = {
+            'summary': model.what,
+            'description': model.html,
+            'location': model.where,
+            'anyoneCanAddSelf': True,
+        }
 
         if model.all_day:
-            start_time = self._make_time(model.start_date)
-            if model.start_date == model.end_date:
-                end_time = None
-            else:
-                end_time = self._make_time(model.end_date)
+            start = {'date': model.start_date.isoformat()}
+            end = {'date': model.end_date.isoformat()}
         else:
-            start_time = self._make_time(model.start_date, model.start_time, model.time_zone)
-            end_time = self._make_time(model.end_date, model.end_time, model.time_zone)
+            start = {'dateTime': self._make_time(model.start_date, model.start_time,
+                                                 model.time_zone)}
+            end = {'dateTime': self._make_time(model.end_date, model.end_time,
+                                               model.time_zone)}
 
-        event.when = [When(start=start_time, end=end_time)]
+        event['start'] = start
+        event['end'] = end
         return event
 
     def _make_time(self, date, time=None, tz_name=None):
@@ -126,23 +124,20 @@
         no time zone info will be added to the string.
         """
 
-        if time is not None:
+        if time:
             d = datetime.datetime.combine(date, time)
         else:
             d = datetime.datetime(date.year, date.month, date.day)
 
-        if time is None:
-            s = d.strftime(self.DATE_FMT)
-        elif tz_name is None:
+        if not tz_name:
             s = d.strftime(self.DATE_TIME_FMT)
         else:
             try:
                 tz = pytz.timezone(tz_name)
             except pytz.UnknownTimeZoneError:
-                raise CalendarError('Invalid time zone: %s' (tz_name,))
+                raise CalendarError('Invalid time zone: %s' % tz_name)
             local = tz.localize(d)
             zulu = local.astimezone(FixedOffset(0))
             s = zulu.strftime(self.DATE_TIME_TZ_FMT)
 
         return s
-
--- a/gcalendar/models.py	Thu Nov 20 18:47:54 2014 -0600
+++ b/gcalendar/models.py	Sat Nov 22 13:27:13 2014 -0600
@@ -2,15 +2,12 @@
 Models for the gcalendar application.
 
 """
-import datetime
-
 from django.db import models
 from django.db.models import Q
 from django.contrib.auth.models import User
 
 from core.markup import site_markup
 import forums.tools
-from gcalendar.oauth import serialize_token, deserialize_token
 
 
 GIG_FORUM_SLUG = "gigs"
@@ -72,7 +69,7 @@
         ordering = ('-date_submitted', )
 
     def save(self, *args, **kwargs):
-        self.html = site_markup(self.description)
+        self.html = site_markup(self.description, relative_urls=False)
         super(Event, self).save(*args, **kwargs)
 
     def is_approved(self):
@@ -107,53 +104,3 @@
 
             self.create_forum_thread = False
             self.save()
-
-
-class AccessTokenManager(models.Manager):
-    """
-    A manager for the AccessToken table. Only one access token is saved in the
-    database. This manager provides a convenience method to either return that
-    access token or a brand new one.
-
-    """
-    def get_token(self):
-        try:
-            token = self.get(pk=1)
-        except AccessToken.DoesNotExist:
-            token = AccessToken()
-
-        return token
-
-
-class AccessToken(models.Model):
-    """
-    This model represents serialized OAuth access tokens for reading and
-    updating the Google Calendar.
-
-    """
-    auth_date = models.DateTimeField()
-    token = models.TextField()
-
-    objects = AccessTokenManager()
-
-    def __unicode__(self):
-        return u'Access token created on ' + unicode(self.auth_date)
-
-    def update(self, access_token, auth_date=None):
-        """
-        This function updates the AccessToken object with the input parameters:
-            access_token - an access token from Google's OAuth dance
-            auth_date - a datetime or None. If None, now() is used.
-
-        """
-        self.auth_date = auth_date if auth_date else datetime.datetime.now()
-        self.token = serialize_token(access_token)
-
-    def access_token(self):
-        """
-        This function returns a Google OAuth access token by deserializing the
-        token field from the database.
-        If the token attribute is empty, None is returned.
-
-        """
-        return deserialize_token(self.token) if self.token else None
--- a/gcalendar/oauth.py	Thu Nov 20 18:47:54 2014 -0600
+++ b/gcalendar/oauth.py	Sat Nov 22 13:27:13 2014 -0600
@@ -28,19 +28,16 @@
     """
     try:
         status = os.stat(settings.GCAL_CREDENTIALS_PATH)
-        return datetime.datetime.fromtimestamp(status.st_mtime)
     except OSError:
         return None
+    return datetime.datetime.fromtimestamp(status.st_mtime)
 
 
-def get_auth_url(request, callback_url):
+def get_auth_url(callback_url):
     """
     This function creates an OAuth flow object and uses it to generate an
     authorization URL which is returned to the caller.
 
-    request - the HttpRequest object for the user requesting the token. The
-    token is stored in the session object attached to this request.
-
     callback_url - a string that is the URL that Google should redirect the user
     to after the user has authorized our application access to their data.
 
@@ -69,7 +66,7 @@
 
 def auth_return(request):
     """
-    This function should be called after Google has sent the user back to us
+    This function should be called after Google has redirected the user
     after the user authorized us. We retrieve the authorization code from the
     request query parameters and then exchange it for access tokens. These
     tokens are stored in our credentials file.
@@ -111,18 +108,7 @@
     logging.info("auth_return completed successfully")
 
 
-def serialize_token(token):
-    """
-    This function turns a token into a string and returns it.
-
-    """
-    return gdata.gauth.TokenToBlob(token)
-
-
-def deserialize_token(s):
-    """
-    This function turns a string into a token returns it. The string must have
-    previously been created with serialize_token().
-
-    """
-    return gdata.gauth.TokenFromBlob(s)
+def get_credentials():
+    """Obtain the stored credentials if available, or None if they are not."""
+    storage = Storage(settings.GCAL_CREDENTIALS_PATH)
+    return storage.get()
--- a/sg101/templates/gcalendar/google_sync.html	Thu Nov 20 18:47:54 2014 -0600
+++ b/sg101/templates/gcalendar/google_sync.html	Sat Nov 22 13:27:13 2014 -0600
@@ -33,7 +33,7 @@
 {% endfor %}
 </ol>
 
-{% if access_token %}
+{% if cred_status %}
 <form action="." method="POST">{% csrf_token %}
    <p><input type="submit" name="synchronize" value="Synchronize Events" /></p>
 </form>