+#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")
+ return None
+ if event.recurrence is None:
+ logger.error("Empty recurrence structure")
+ return None
+ 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
+ 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
+ if event.is_all_day:
+ gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
+ gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
+ else:
+ gevent["end"]={"dateTime": event.end.astimezone(gcal_tz).isoformat(),
+ "timeZone": str(gcal_tz)}
+ gevent["start"]={"dateTime": event.start.astimezone(gcal_tz).isoformat(),
+ "timeZone": str(gcal_tz)}
+ if event.text_body is not None and event.text_body.strip() != '':
+ gevent["description"] = event.text_body
+ if event.location is not None:
+ gevent["location"] = event.location
+ gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
+ return gevent