chiark / gitweb /
check event isn't already cancelled before deleting
[gooswapper] / gooswapper.py
index a4029f0971779d625a847e7338f31d363cea1c8e..9f4cefa271a7ff60e1ef96d04db36b070e419a98 100644 (file)
@@ -54,7 +54,18 @@ class ex_gcal_link(exchangelib.ExtendedProperty):
     property_name = "google calendar event id"
     property_type = 'String'
 
-exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
+try:
+    exchangelib.CalendarItem.get_field_by_fieldname('gcal_link')
+except ValueError:
+    exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
+
+#useful if you want to replay an event
+def drop_from_ex_cache(itemid):
+    with open(cachepath,"rb") as f:
+        cache = pickle.load(f)
+    cache.pop(itemid)
+    with open(cachepath,"wb") as f:
+        pickle.dump(cache,f)
 
 def get_ex_event_by_itemid(calendar,itemid):
     return calendar.get(item_id=itemid)
@@ -63,6 +74,20 @@ def get_ex_event_by_id_and_changekey(acct,itemid,changekey):
     l=list(acct.fetch([(itemid,changekey)]))
     return list(acct.fetch([(itemid,changekey)]))[0]
 
+def get_gcal_event_by_eventid(gcal_acct,eventId,gcal_id="primary"):
+    return gcal_acct.events().get(calendarId=gcal_id,eventId=eventId).execute()
+
+def get_gcal_recur_instance(gcal_acct,gcal_master,start,gcal_id="primary"):
+    ans = gcal_acct.events().instances(calendarId=gcal_id,
+                                       eventId=gcal_master,
+                                       originalStart=start.isoformat(),
+                                       showDeleted=True).execute()
+    if len(ans['items']) != 1:
+        logger.error("Searching for recurrance instance returned %d events" % \
+                     len(ans['items']))
+        return None
+    return ans['items'][0]
+
 def get_ex_cred(username="SANGER\mv3",password=None):
     if password is None:
         password = getpass.getpass(prompt="Password for user %s: " % username)
@@ -125,6 +150,21 @@ def ex_event_changes(old,new):
                                                               len(deleted)))
     return added, deleted, changed
 
+#exchangelib gives us days in recurrence patterns as integers,
+#RFC5545 wants SU,MO,TU,WE,TH,FR,SA
+#it has a utility function to convert to Monday, Tuesday, ...
+def rr_daystr_from_int(i):
+    return exchangelib.recurrence._weekday_to_str(i).upper()[:2]
+
+#for monthly patterns, we want the week (or -1 for last) combined with each
+#day specified
+def rr_daystr_monthly(p):
+    if p.week_number == 5:
+        wn = "-1"
+    else:
+        wn = str(p.week_number)
+    return ",".join([wn + rr_daystr_from_int(x) for x in p.weekdays])
+
 def rrule_from_ex(event,gcal_tz):
     if event.type != "RecurringMaster":
         logger.error("Cannot make recurrence from not-recurring event")
@@ -135,20 +175,75 @@ def rrule_from_ex(event,gcal_tz):
     if isinstance(event.recurrence.pattern,
                   exchangelib.recurrence.DailyPattern):
         rr = "RRULE:FREQ=DAILY;INTERVAL=%d" % event.recurrence.pattern.interval
+    elif isinstance(event.recurrence.pattern,
+                    exchangelib.recurrence.WeeklyPattern):
+        rr = "RRULE:FREQ=WEEKLY;INTERVAL=%d;BYDAY=%s;WKST=%s" % \
+                          (event.recurrence.pattern.interval,
+                           ",".join([rr_daystr_from_int(x) for x in event.recurrence.pattern.weekdays]),
+                           rr_daystr_from_int(event.recurrence.pattern.first_day_of_week) )
+    elif isinstance(event.recurrence.pattern,
+                    exchangelib.recurrence.RelativeMonthlyPattern):
+        rr = "RRULE:FREQ=MONTHLY;INTERVAL=%d;BYDAY=%s" % \
+                     (event.recurrence.pattern.interval,
+                      rr_daystr_monthly(event.recurrence.pattern))
+    elif isinstance(event.recurrence.pattern,
+                    exchangelib.recurrence.AbsoluteMonthlyPattern):
+        rr = "RRULE:FREQ=MONTHLY;INTERVAL=%d;BYMONTHDAY=%d" % \
+                          (event.recurrence.pattern.interval,
+                           event.recurrence.pattern.day_of_month)
+    elif isinstance(event.recurrence.pattern,
+                    exchangelib.recurrence.AbsoluteYearlyPattern):
+        rr = "RRULE:FREQ=YEARLY;BYMONTH=%d;BYMONTHDAY=%d" % \
+                          (event.recurrence.pattern.month,
+                           event.recurrence.pattern.day_of_month)
+    elif isinstance(event.recurrence.pattern,
+                    exchangelib.recurrence.RelativeYearlyPattern):
+        rr = "RRULE:FREQ=YEARLY;BYMONTH=%d;BYDAY=%s" % \
+                          (event.recurrence.pattern.month,
+                           rr_daystr_monthly(event.recurrence.pattern))
     else:
         logger.error("Recurrence %s not supported" % event.recurrence)
         return None
     if isinstance(event.recurrence.boundary,
                   exchangelib.recurrence.EndDatePattern):
         rr += ";UNTIL={0:%Y}{0:%m}{0:%d}".format(event.recurrence.boundary.end)
+    elif isinstance(event.recurrence.boundary,
+                    exchangelib.recurrence.NoEndPattern):
+        pass #no end date to set
     else:
         logger.error("Recurrence %s not supported" % event.recurrence)
         return None
-    if event.modified_occurrences is not None or \
-       event.deleted_occurrences is not None:
-        logger.warning("Modified/Deleted recurrences not supported")
     return [rr]
 
+def modify_recurring(ex_acct,gcal_acct,gcal_tz,
+                     events,master,gcal_id="primary"):
+    if master.modified_occurrences is not None:
+        for mod in master.modified_occurrences:
+            instance = get_gcal_recur_instance(gcal_acct,master.gcal_link,
+                                               mod.original_start,gcal_id)
+            if instance is None: #give up after first failure
+                return
+            mod_event = get_ex_event_by_itemid(ex_acct.calendar,mod.item_id)
+            gevent = build_gcal_event_from_ex(mod_event,gcal_tz)
+            gevent = gcal_acct.events().update(calendarId=gcal_id,
+                                               eventId=instance.get('id'),
+                                               body=gevent,
+                                               sendUpdates="none").execute()
+            mod_event.gcal_link = gevent.get("id")
+            mod_event.save(update_fields=["gcal_link"])
+    if master.deleted_occurrences is not None:
+        for d in master.deleted_occurrences:
+            instance = get_gcal_recur_instance(gcal_acct,master.gcal_link,
+                                               d.start,gcal_id)
+            if instance is None: #give up after any failure
+                return
+            if instance["status"] != "cancelled":
+                instance["status"]="cancelled"
+                gcal_acct.events().update(calendarId=gcal_id,
+                                          eventId=instance.get('id'),
+                                          body=instance,
+                                          sendUpdates="none").execute()
+
 def build_gcal_event_from_ex(event,gcal_tz):
     gevent={}
     gevent["summary"]=event.subject
@@ -180,19 +275,30 @@ def add_ex_to_gcal(ex_acct,
                 gevent["recurrence"] = rr
                 print(gevent)
             else:
-                logger.warning("Unable to set recurrence")
+                logger.warning("Unable to set recurrence for %s" % event.item_id)
+                continue #don't make the gcal event
         gevent = gcal_acct.events().insert(calendarId=gcal_id,
                                            body=gevent).execute()
         event.gcal_link = gevent.get("id")
         event.save(update_fields=["gcal_link"])
+        if event.type=="RecurringMaster" and (event.deleted_occurrences or \
+                                              event.modified_occurrences):
+            modify_recurring(ex_acct,gcal_acct,gcal_tz,
+                             events,event,gcal_id)
+            #changekey is updated by the above
+            event.refresh()
         events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link)
-
+        
 def del_ex_to_gcal(ex_acct, gcal_acct, events, deleted, gcal_id="primary"):
     for ev_id in deleted:
         if events[ev_id].gcal_link is not None:
-            gcal_acct.events().delete(calendarId=gcal_id,
-                                      eventId=events[ev_id].gcal_link,
-                                      sendUpdates="none").execute()
+            gevent = get_gcal_event_by_eventid(gcal_acct,
+                                               events[ev_id].gcal_link,
+                                               gcal_id)
+            if gevent["status"] != "cancelled":
+                gcal_acct.events().delete(calendarId=gcal_id,
+                                          eventId=events[ev_id].gcal_link,
+                                          sendUpdates="none").execute()
 
 def update_ex_to_gcal(ex_acct,
                       gcal_acct,gcal_tz,
@@ -200,14 +306,24 @@ def update_ex_to_gcal(ex_acct,
                       gcal_id="primary"):
     for ev_id in changed:
         event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
-        if not event.is_recurring:
-            gevent = build_gcal_event_from_ex(event,gcal_tz)
-            gevent = gcal_acct.events().update(calendarId=gcal_id,
+        gevent = build_gcal_event_from_ex(event,gcal_tz)
+        if event.type=="RecurringMaster":
+            rr = rrule_from_ex(event,gcal_tz)
+            if rr is not None:
+                gevent["recurrence"] = rr
+                if event.deleted_occurrences or \
+                   event.modified_occurrences:
+                    modify_recurring(ex_acct,gcal_acct,gcal_tz,
+                                     events,event,gcal_id)
+                    event.refresh() #changekey is updated by the above
+                    events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link)
+            else:
+                logger.warning("Unable to set recurrence for %s" % event.item_id)
+                continue #don't make the gcal event
+        gevent = gcal_acct.events().update(calendarId=gcal_id,
                                                eventId=event.gcal_link,
                                                body=gevent,
                                                sendUpdates="none").execute()
-        else:
-            logger.warning("recurring events not yet supported")
 
 def match_ex_to_gcal(ex_acct,gcal_acct,gcal_tz,events,gcal_id="primary"):
     recur = 0