comparison gcalendar/calendar.py @ 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 ee87ea74d46b
children 4dc6a452afd3
comparison
equal deleted inserted replaced
856:c2dfd1b1323e 857:9165edfb1709
1 """ 1 """
2 This file contains the calendar class wich abstracts the Google gdata API for working with 2 This file contains the calendar class wich abstracts the Google API for working
3 Google Calendars. 3 with Google Calendars.
4 4
5 """ 5 """
6 import datetime 6 import datetime
7 import pytz 7 import pytz
8 8
9 import httplib2
10 from apiclient.discovery import build
11 from apiclient.http import BatchHttpRequest
9 from django.utils.tzinfo import FixedOffset 12 from django.utils.tzinfo import FixedOffset
10 from gdata.calendar.client import CalendarClient
11 from gdata.calendar.data import (CalendarEventEntry, CalendarEventFeed,
12 CalendarWhere, When, EventWho)
13 import atom.data
14 13
15 from gcalendar.models import Event 14 from gcalendar.models import Event
16 15
17 16
18 class CalendarError(Exception): 17 class CalendarError(Exception):
19 def __init__(self, msg): 18 """Calendar exception base class."""
20 self.msg = msg
21 19
22 def __str__(self): 20
23 return repr(self.msg) 21 def _make_err_msg(event, exception):
22 """Returns an error message string from the given Event and exception."""
23 return '%s - %s' % (event.what, exception)
24 24
25 25
26 class Calendar(object): 26 class Calendar(object):
27 DATE_FMT = '%Y-%m-%d' 27 DATE_TIME_FMT = '%Y-%m-%dT%H:%M:%S'
28 DATE_TIME_FMT = DATE_FMT + 'T%H:%M:%S' 28 DATE_TIME_TZ_FMT = DATE_TIME_FMT + 'Z'
29 DATE_TIME_TZ_FMT = DATE_TIME_FMT + '.000Z'
30 29
31 def __init__(self, source=None, calendar_id='default', access_token=None): 30 def __init__(self, calendar_id='primary', credentials=None):
32 self.client = CalendarClient(source=source, auth_token=access_token) 31 self.calendar_id = calendar_id
33 32 http = httplib2.Http()
34 self.insert_feed = ('https://www.google.com/calendar/feeds/' 33 if credentials:
35 '%s/private/full' % calendar_id) 34 http = credentials.authorize(http)
36 self.batch_feed = '%s/batch' % self.insert_feed 35 self.service = build('calendar', 'v3', http=http)
37 36
38 def sync_events(self, qs): 37 def sync_events(self, qs):
39 request_feed = CalendarEventFeed() 38 """Process the pending events in a batch request to the Google calendar
40 for model in qs: 39 API.
41 if model.status == Event.NEW_APRV: 40 """
42 event = CalendarEventEntry() 41 batch = BatchHttpRequest()
43 request_feed.AddInsert(entry=self._populate_event(model, event)) 42
44 elif model.status == Event.EDIT_APRV: 43 err_msgs = []
45 event = self._retrieve_event(model) 44 def insert_callback(request_id, event, exception):
46 request_feed.AddUpdate(entry=self._populate_event(model, event)) 45 n = int(request_id)
47 elif model.status == Event.DEL_APRV: 46 if not exception:
48 event = self._retrieve_event(model) 47 qs[n].status = Event.ON_CAL
49 request_feed.AddDelete(entry=event) 48 qs[n].google_id = event['id']
49 qs[n].google_url = event['htmlLink']
50 qs[n].save()
51 qs[n].notify_on_calendar()
50 else: 52 else:
51 assert False, 'unexpected status in sync_events' 53 err_msgs.append(_make_err_msg(qs[n], exception))
54
55 def update_callback(request_id, event, exception):
56 n = int(request_id)
57 if not exception:
58 qs[n].status = Event.ON_CAL
59 qs[n].save()
60 else:
61 err_msgs.append(_make_err_msg(qs[n], exception))
62
63 def delete_callback(request_id, response, exception):
64 n = int(request_id)
65 if not exception:
66 qs[n].delete()
67 else:
68 err_msgs.append(_make_err_msg(qs[n], exception))
69
70 for n, event in enumerate(qs):
71 if event.status == Event.NEW_APRV:
72 batch.add(self.service.events().insert(calendarId=self.calendar_id,
73 body=self._make_event(event)),
74 callback=insert_callback,
75 request_id=str(n))
76 elif event.status == Event.EDIT_APRV:
77 batch.add(self.service.events().update(calendarId=self.calendar_id,
78 eventId=event.google_id,
79 body=self._make_event(event)),
80 callback=update_callback,
81 request_id=str(n))
82 elif event.status == Event.DEL_APRV:
83 batch.add(self.service.events().delete(calendarId=self.calendar_id,
84 eventId=event.google_id),
85 callback=delete_callback,
86 request_id=str(n))
87 else:
88 raise CalendarError("Invalid event status: %s" % event.status)
52 89
53 try: 90 try:
54 response_feed = self.client.ExecuteBatch(request_feed, self.batch_feed) 91 batch.execute()
55 except Exception, e: 92 except Exception as ex:
56 raise CalendarError('ExecuteBatch exception: %s' % e) 93 raise CalendarError('Batch exception: %s' % ex)
57 94
58 err_msgs = [] 95 if err_msgs:
59 for entry in response_feed.entry:
60 i = int(entry.batch_id.text)
61 code = int(entry.batch_status.code)
62
63 error = False
64 if qs[i].status == Event.NEW_APRV:
65 if code == 201:
66 qs[i].status = Event.ON_CAL
67 qs[i].google_id = entry.GetEditLink().href
68 qs[i].google_url = entry.GetHtmlLink().href
69 qs[i].save()
70 qs[i].notify_on_calendar()
71 else:
72 error = True
73
74 elif qs[i].status == Event.EDIT_APRV:
75 if code == 200:
76 qs[i].status = Event.ON_CAL
77 qs[i].save()
78 else:
79 error = True
80
81 elif qs[i].status == Event.DEL_APRV:
82 if code == 200:
83 qs[i].delete()
84 else:
85 error = True
86
87 if error:
88 err_msgs.append('%s - (%d) %s' % (
89 qs[i].what, code, entry.batch_status.reason))
90
91 if len(err_msgs) > 0:
92 raise CalendarError(', '.join(err_msgs)) 96 raise CalendarError(', '.join(err_msgs))
93 97
94 def _retrieve_event(self, model): 98 def _make_event(self, model):
95 try: 99 """Creates an event body from a model instance."""
96 event = self.client.GetEventEntry(model.google_id) 100 event = {
97 except Exception, e: 101 'summary': model.what,
98 raise CalendarError('Could not retrieve event from Google: %s, %s' \ 102 'description': model.html,
99 % (model.what, e)) 103 'location': model.where,
100 return event 104 'anyoneCanAddSelf': True,
101 105 }
102 def _populate_event(self, model, event):
103 """Populates a gdata event from an Event model object."""
104 event.title = atom.data.Title(text=model.what)
105 event.content = atom.data.Content(text=model.html)
106 event.where = [CalendarWhere(value=model.where)]
107 event.who = [EventWho(email=model.user.email)]
108 106
109 if model.all_day: 107 if model.all_day:
110 start_time = self._make_time(model.start_date) 108 start = {'date': model.start_date.isoformat()}
111 if model.start_date == model.end_date: 109 end = {'date': model.end_date.isoformat()}
112 end_time = None
113 else:
114 end_time = self._make_time(model.end_date)
115 else: 110 else:
116 start_time = self._make_time(model.start_date, model.start_time, model.time_zone) 111 start = {'dateTime': self._make_time(model.start_date, model.start_time,
117 end_time = self._make_time(model.end_date, model.end_time, model.time_zone) 112 model.time_zone)}
113 end = {'dateTime': self._make_time(model.end_date, model.end_time,
114 model.time_zone)}
118 115
119 event.when = [When(start=start_time, end=end_time)] 116 event['start'] = start
117 event['end'] = end
120 return event 118 return event
121 119
122 def _make_time(self, date, time=None, tz_name=None): 120 def _make_time(self, date, time=None, tz_name=None):
123 """ 121 """
124 Returns the gdata formatted date/time string given a date, optional time, 122 Returns the gdata formatted date/time string given a date, optional time,
125 and optional time zone name (e.g. 'US/Pacific'). If the time zone name is None, 123 and optional time zone name (e.g. 'US/Pacific'). If the time zone name is None,
126 no time zone info will be added to the string. 124 no time zone info will be added to the string.
127 """ 125 """
128 126
129 if time is not None: 127 if time:
130 d = datetime.datetime.combine(date, time) 128 d = datetime.datetime.combine(date, time)
131 else: 129 else:
132 d = datetime.datetime(date.year, date.month, date.day) 130 d = datetime.datetime(date.year, date.month, date.day)
133 131
134 if time is None: 132 if not tz_name:
135 s = d.strftime(self.DATE_FMT)
136 elif tz_name is None:
137 s = d.strftime(self.DATE_TIME_FMT) 133 s = d.strftime(self.DATE_TIME_FMT)
138 else: 134 else:
139 try: 135 try:
140 tz = pytz.timezone(tz_name) 136 tz = pytz.timezone(tz_name)
141 except pytz.UnknownTimeZoneError: 137 except pytz.UnknownTimeZoneError:
142 raise CalendarError('Invalid time zone: %s' (tz_name,)) 138 raise CalendarError('Invalid time zone: %s' % tz_name)
143 local = tz.localize(d) 139 local = tz.localize(d)
144 zulu = local.astimezone(FixedOffset(0)) 140 zulu = local.astimezone(FixedOffset(0))
145 s = zulu.strftime(self.DATE_TIME_TZ_FMT) 141 s = zulu.strftime(self.DATE_TIME_TZ_FMT)
146 142
147 return s 143 return s
148