view gcalendar/calendar.py @ 861:e4f8d87c3d30

Configure Markdown logger to reduce noise in logs. Markdown is logging at the INFO level whenever it loads an extension. This looks like it has been fixed in master at GitHub. But until then we will explicitly configure the MARKDOWN logger to log at WARNING or higher.
author Brian Neal <bgneal@gmail.com>
date Mon, 01 Dec 2014 18:36:27 -0600
parents 9165edfb1709
children 4dc6a452afd3
line wrap: on
line source
"""
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 gcalendar.models import Event


class CalendarError(Exception):
    """Calendar exception base class."""


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_TIME_FMT = '%Y-%m-%dT%H:%M:%S'
    DATE_TIME_TZ_FMT = DATE_TIME_FMT + 'Z'

    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):
        """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:
                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:
            batch.execute()
        except Exception as ex:
            raise CalendarError('Batch exception: %s' % ex)

        if err_msgs:
            raise CalendarError(', '.join(err_msgs))

    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 = {'date': model.start_date.isoformat()}
            end = {'date': model.end_date.isoformat()}
        else:
            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['start'] = start
        event['end'] = end
        return event

    def _make_time(self, date, time=None, tz_name=None):
        """
        Returns the gdata formatted date/time string given a date, optional time,
        and optional time zone name (e.g. 'US/Pacific'). If the time zone name is None,
        no time zone info will be added to the string.
        """

        if time:
            d = datetime.datetime.combine(date, time)
        else:
            d = datetime.datetime(date.year, date.month, date.day)

        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)
            local = tz.localize(d)
            zulu = local.astimezone(FixedOffset(0))
            s = zulu.strftime(self.DATE_TIME_TZ_FMT)

        return s