1 """
2 Syncronize with Google Calendar
3
4 This file is part of Pisi.
5
6 Pisi is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Pisi is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Pisi. If not, see <http://www.gnu.org/licenses/>
18 """
19
20 import gdata.calendar.service
21 import gdata.service
22 import atom
23 import sys,os
24 import datetime,time
25
26 import pisiprogress
27 from pisiconstants import *
28 from events import events
29
31 """
32 The implementation of the interface L{events.AbstractCalendarSynchronizationModule} for the Google Calendar backend
33 """
34 - def __init__( self, modulesString, config, configsection, folder, verbose=False, soft=False):
35 """
36 Constructor
37
38 Super class constructor (L{events.AbstractCalendarSynchronizationModule.__init__}) is called.
39 Local variables are initialized.
40 The settings from the configuration file are loaded.
41 The connection to the Google Gdata backend is established.
42 """
43 events.AbstractCalendarSynchronizationModule.__init__(self, verbose, soft, modulesString, config, configsection, "Google Calendar")
44 self.folder = folder
45 self.newEvents = {}
46 self._googleevents = {}
47 self.batchOperations = gdata.calendar.CalendarEventFeed()
48 user = config.get(configsection,'user')
49 password = config.get(configsection, 'password')
50 self.calendarid = config.get(configsection,'calendarid')
51 self._login( user, password )
52
54 """
55 Load all data from backend
56
57 A single query is performed and the result set is parsed afterwards.
58 """
59 feed = self.cal_client.GetCalendarEventFeed('/calendar/feeds/'+self.calendarid+'/private/full?max-results=%d' %(GOOGLE_CALENDAR_MAXRESULTS))
60 for i, an_event in enumerate(feed.entry):
61 globalId, updated, attributes = self._geventToPisiEvent(an_event)
62 if globalId == None:
63 globalId = events.assembleID()
64 tmpEvent = events.Event( globalId, updated, attributes )
65 tmpEvent.attributes['globalid'] = globalId
66 self.replaceEvent(globalId, tmpEvent)
67 else:
68 tmpEvent = events.Event( globalId, updated, attributes)
69 self._allEvents[globalId] = tmpEvent
70 self._googleevents[globalId] = an_event
71
73 """
74 Saves an event as part of saving modifications
75 """
76 eventInstance = self.getEvent(id)
77 gevent = self._convertPisiEventToGoogle(eventInstance)
78 gevent.batch_id = gdata.BatchId(text=eventInstance.id)
79 self.batchOperations.AddInsert(entry=gevent)
80
82 """
83 Replace event as part of saving modifications
84 """
85 updatedevent = self.getEvent(id)
86 gevent = self._convertPisiEventToGoogle(updatedevent)
87 gevent.batch_id = gdata.BatchId(text=id)
88 gevent.id = atom.Id( id )
89 editUri = self._googleevents[id].GetEditLink().href
90 gevent.link.append( atom.Link(editUri, 'edit', 'application/atom+xml') )
91 self.batchOperations.AddUpdate(gevent)
92
94 """
95 Removes an event as part of saving modifications
96 """
97 self._googleevents[id].batch_id = gdata.BatchId(text=id)
98 self.batchOperations.AddDelete(entry=self._googleevents[id])
99
101 """
102 Makes changes permanent
103 """
104 pisiprogress.getCallback().verbose("Commiting Google-calendar modifications")
105 response_feed = self.cal_client.ExecuteBatch(self.batchOperations, '/calendar/feeds/'+self.calendarid+'/private/full/batch')
106 for entry in response_feed.entry:
107 try:
108 pisiprogress.getCallback().verbose('batch id: %s' % (entry.batch_id.text))
109 pisiprogress.getCallback().verbose('status: %s' % (entry.batch_status.code))
110 pisiprogress.getCallback().verbose('reason: %s' % (entry.batch_status.reason))
111 except AttributeError:
112 print "<<", entry.content.text, entry.title.text, "\n", entry
113
142
143
145 """
146 Supporting function to assemble a date-time-object depending on the type of event (all day or special times)
147 """
148 if not dateTimeObject:
149 return None
150 if allday:
151 return dateTimeObject.strftime('%Y-%m-%d')
152 else:
153 dt = dateTimeObject.strftime('%Y-%m-%dT%H:%M:%S.000%z')
154 if dateTimeObject.tzinfo:
155 dt = dt[:26] + ":" + dt[26:]
156 else:
157 dt+="Z"
158 return dt
159
161 """
162 Supporting function to convert a PISI event (internal format) into a Google Gdata Calendar event
163 """
164 gevent = gdata.calendar.CalendarEventEntry()
165 gevent.title = atom.Title(text=event.attributes['title'])
166 gevent.where.append(gdata.calendar.Where(value_string=event.attributes['location']))
167 gevent.when.append(gdata.calendar.When(\
168 start_time=self._convertToGoogle(event.attributes['start'],event.attributes['allday']), \
169 end_time=self._convertToGoogle(event.attributes['end'],event.attributes['allday'])))
170 gevent.content = atom.Content(text=event.attributes['description'])
171 gevent.extended_property.append(gdata.calendar.ExtendedProperty(name='pisiid', value=event.attributes['globalid'] ))
172 if event.attributes.has_key('recurrence') and event.attributes['recurrence']:
173 gevent.recurrence = gdata.calendar.Recurrence(text=event.attributes['recurrence'].getData())
174 return gevent
175
177 """
178 Converts a Google event to Pisi event (internal format)
179 """
180 if event.recurrence:
181 recurrence = events.Recurrence()
182 recurrence.initFromData(event.recurrence.text)
183
184 start = recurrence.getDTStart()
185 end = recurrence.getDTEnd()
186 allday = recurrence.isAllDay()
187 else:
188 recurrence = None
189 (allday, start) = self._gtimeToDatetime(event.when[0].start_time )
190 (allday, end) = self._gtimeToDatetime(event.when[0].end_time )
191 attributes=\
192 {'start':start, \
193 'end':end, \
194 'recurrence':recurrence, \
195 'allday':allday, \
196 'title':event.title.text, \
197 'description':event.content.text, \
198 'location':event.where[0].value_string,\
199 'alarm':False, 'alarmmin':0 \
200 }
201 (tmp, updated) = self._gtimeToDatetime(event.updated.text )
202
203 pisiID = None
204 try:
205 for prop in event.extended_property:
206 if prop.name == 'pisiid':
207 pisiID = prop.value
208 attributes['globalid'] = pisiID
209 except BaseException:
210 pass
211 return pisiID, updated, attributes
212
214 """
215 Converts Google normal way (RFC3339) to write date and time to a datetime instance
216 """
217 allday = False
218 if len(gtime)==10:
219 allday = True
220 date = datetime.datetime(int(gtime[0:4]), int(gtime[5:7]), int(gtime[8:10]), tzinfo = events.UTC())
221 return (allday, date.date())
222
223 if gtime[23]=='Z':
224 tz = events.UTC()
225 else:
226 tz = events.CustomOffset("custom", gtime[23:])
227 onlyDandT = datetime.datetime(int(gtime[0:4]), int(gtime[5:7]), int(gtime[8:10]), int(gtime[11:13]), int(gtime[14:16]), int(gtime[17:19]), 0, tz)
228 return (allday, onlyDandT)
229
230 - def _login( self, user, password ):
231 """
232 Supporting function to perform login at Google's Calendar Web-Service API
233 """
234 pisiprogress.getCallback().verbose("Google Calendar: Logging in with user %s" %(user))
235 self.cal_client = gdata.calendar.service.CalendarService()
236 self.cal_client.email = user
237 self.cal_client.password = password
238 self.cal_client.source = GOOGLE_CALENDAR_APPNAME
239 self.cal_client.ProgrammaticLogin()
240