# HG changeset patch # User Brian Neal # Date 1416684433 21600 # Node ID 9165edfb17091f1dac5c1d02c49035264a5c4107 # Parent c2dfd1b1323e9e1af7f9175352ecb85efd83f631 For issue #80, use new Google Calendar v3 API. diff -r c2dfd1b1323e -r 9165edfb1709 gcalendar/admin.py --- 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) diff -r c2dfd1b1323e -r 9165edfb1709 gcalendar/calendar.py --- 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 - diff -r c2dfd1b1323e -r 9165edfb1709 gcalendar/models.py --- 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 diff -r c2dfd1b1323e -r 9165edfb1709 gcalendar/oauth.py --- 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() diff -r c2dfd1b1323e -r 9165edfb1709 sg101/templates/gcalendar/google_sync.html --- 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 %} -{% if access_token %} +{% if cred_status %}
{% csrf_token %}