# HG changeset patch # User Paul Boddie # Date 1427500512 -3600 # Node ID 00fcdf47658c6d07c44a48fd4b3d93c62ed8f02f # Parent c0775c5083ffca55cc0d225842da34c173389ba8 Introduced abstractions to make working with different forms of periods easier (free/busy, recurring, simple periods). diff -r c0775c5083ff -r 00fcdf47658c imip_store.py --- a/imip_store.py Sat Mar 28 00:52:14 2015 +0100 +++ b/imip_store.py Sat Mar 28 00:55:12 2015 +0100 @@ -23,6 +23,7 @@ from imiptools.config import STORE_DIR, PUBLISH_DIR from imiptools.data import make_calendar, parse_object, to_stream from imiptools.filesys import fix_permissions, FileBase +from imiptools.period import FreeBusyPeriod from os.path import exists, isfile, join from os import listdir, remove, rmdir from time import sleep @@ -316,7 +317,7 @@ if not filename or not exists(filename): return [] else: - return self._get_table(user, filename, [(4, None)]) + return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) def get_freebusy_for_other(self, user, other): @@ -326,7 +327,7 @@ if not filename or not exists(filename): return [] else: - return self._get_table(user, filename, [(4, None)]) + return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)])) def set_freebusy(self, user, freebusy): @@ -336,7 +337,8 @@ if not filename: return False - self._set_table(user, filename, freebusy, [(3, "OPAQUE"), (4, "")]) + self._set_table(user, filename, map(lambda fb: fb.as_tuple(), freebusy), + [(3, "OPAQUE"), (4, "")]) return True def set_freebusy_for_other(self, user, freebusy, other): @@ -347,7 +349,8 @@ if not filename: return False - self._set_table(user, filename, freebusy, [(2, ""), (3, "OPAQUE"), (4, "")]) + self._set_table(user, filename, map(lambda fb: fb.as_tuple(), freebusy), + [(2, ""), (3, "OPAQUE"), (4, "")]) return True def _get_requests(self, user, queue): @@ -515,9 +518,9 @@ rwrite(("UID", {}, user)) rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) - for start, end, uid, transp, recurrenceid, summary, organiser in freebusy: - if not transp or transp == "OPAQUE": - rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) + for fb in freebusy: + if not fb.transp or fb.transp == "OPAQUE": + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([fb.start, fb.end]))) f = open(filename, "wb") try: diff -r c0775c5083ff -r 00fcdf47658c imiptools/data.py --- a/imiptools/data.py Sat Mar 28 00:52:14 2015 +0100 +++ b/imiptools/data.py Sat Mar 28 00:55:12 2015 +0100 @@ -25,7 +25,7 @@ from imiptools.dates import format_datetime, get_datetime, get_duration, \ get_freebusy_period, get_period, to_datetime, \ to_timezone, to_utc_datetime -from imiptools.period import period_overlaps +from imiptools.period import FreeBusyPeriod, Period, period_overlaps from pytz import timezone from vCalendar import iterwrite, parse, ParseError, to_dict, to_node from vRecurrence import get_parameters, get_rule @@ -101,14 +101,14 @@ # Computed results. def has_recurrence(self, tzid, recurrence): - recurrences = [start for start, end in get_periods(self, tzid, recurrence, inclusive=True)] + recurrences = [p.start for p in get_periods(self, tzid, recurrence, inclusive=True)] return recurrence in recurrences - def get_periods(self, tzid, end, origin=False): - return get_periods(self, tzid, end, origin=origin) + def get_periods(self, tzid, end): + return get_periods(self, tzid, end) - def get_periods_for_freebusy(self, tzid, end, origin=False): - periods = self.get_periods(tzid, end, origin) + def get_periods_for_freebusy(self, tzid, end): + periods = self.get_periods(tzid, end) return get_periods_for_freebusy(self, periods, tzid) def get_tzid(self): @@ -158,16 +158,18 @@ # Get a constrained view if start and end limits are specified. - periods = dtstart and dtend and period_overlaps(freebusy, (dtstart, dtend), True) or freebusy + periods = dtstart and dtend and \ + period_overlaps(freebusy, Period(dtstart, dtend), True) or \ + freebusy # Write the limits of the resource. rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, periods[0][0])) rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, periods[-1][1])) - for start, end, uid, transp, recurrenceid, summary, organiser in periods: - if transp == "OPAQUE": - rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) + for p in periods: + if p.transp == "OPAQUE": + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([p.start, p.end]))) return ("VFREEBUSY", {}, record) @@ -368,16 +370,23 @@ def get_tzid(dtstart_attr, dtend_attr): return dtstart_attr.get("TZID") or dtend_attr.get("TZID") -def get_periods(obj, tzid, window_end, inclusive=False, origin=False): +class RecurringPeriod(Period): + + "A period with origin information from the object." + + def __init__(self, start, end, origin): + Period.__init__(self, start, end) + self.origin = origin + + def __repr__(self): + return "RecurringPeriod(%r, %r, %r)" % (self.start, self.end, self.origin) + +def get_periods(obj, tzid, window_end, inclusive=False): """ Return periods for the given object 'obj', confining materialised periods to before the given 'window_end' datetime. If 'inclusive' is set to a true value, any period occurring at the 'window_end' will be included. - - If 'origin' is set to a true value, the property type providing each period - will be included in the results, with each result taking the form - (start, end, property type). """ rrule = obj.get_value("RRULE") @@ -399,8 +408,7 @@ tzid = get_tzid(dtstart_attr, dtend_attr) or tzid if not rrule: - origin_value = origin and ("DTSTART",) or () - periods = [(dtstart, dtend) + origin_value] + periods = [RecurringPeriod(dtstart, dtend, "DTSTART")] else: # Recurrence rules create multiple instances to be checked. # Conflicts may only be assessed within a period defined by policy @@ -409,13 +417,12 @@ selector = get_rule(dtstart, rrule) parameters = get_parameters(rrule) - origin_value = origin and ("RRULE",) or () periods = [] for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS"), inclusive): start = to_timezone(datetime(*start), tzid) end = start + duration - periods.append((start, end) + origin_value) + periods.append(RecurringPeriod(start, end, "RRULE")) # Add recurrence dates. @@ -423,12 +430,11 @@ rdates = obj.get_date_values("RDATE", tzid) if rdates: - origin_value = origin and ("RDATE",) or () for rdate in rdates: if isinstance(rdate, tuple): - periods.add(rdate + origin_value) + periods.add(RecurringPeriod(rdate[0], rdate[1], "RDATE")) else: - periods.add((rdate, rdate + duration) + origin_value) + periods.add(RecurringPeriod(rdate, rdate + duration, "RDATE")) # Return a sorted list of the periods. @@ -442,24 +448,26 @@ if exdates: for exdate in exdates: if isinstance(exdate, tuple): - period = exdate + period = Period(exdate[0], exdate[1]) else: - period = (exdate, exdate + duration) + period = Period(exdate, exdate + duration) i = bisect_left(periods, period) - while i < len(periods) and periods[i][:2] == period: + while i < len(periods) and periods[i] == period: del periods[i] return periods class compare_periods: + + "Compare periods for exception date purposes." + def __init__(self, tzid): self.tzid = tzid + def __call__(self, first, second): - first_start, first_end = first[:2] - second_start, second_end = second[:2] return cmp( - (to_datetime(first_start, self.tzid), to_datetime(first_end, self.tzid)), - (to_datetime(second_start, self.tzid), to_datetime(second_end, self.tzid)) + (to_datetime(first.start, self.tzid), to_datetime(first.end, self.tzid)), + (to_datetime(second.start, self.tzid), to_datetime(second.end, self.tzid)) ) def get_periods_for_freebusy(obj, periods, tzid): @@ -482,11 +490,18 @@ l = [] - for t in periods: - start, end = t[:2] - start, end = get_freebusy_period(start, end, tzid) + for p in periods: + start, end = get_freebusy_period(p.start, p.end, tzid) start, end = [to_timezone(x, "UTC") for x in start, end] - l.append((format_datetime(start), format_datetime(end)) + t[2:]) + + # Create a new period for free/busy purposes with the converted + # datetime information. + + l.append( + FreeBusyPeriod( + format_datetime(start), format_datetime(end), + *p.as_tuple()[2:] + )) return l diff -r c0775c5083ff -r 00fcdf47658c imiptools/period.py --- a/imiptools/period.py Sat Mar 28 00:52:14 2015 +0100 +++ b/imiptools/period.py Sat Mar 28 00:55:12 2015 +0100 @@ -23,6 +23,64 @@ from datetime import datetime, timedelta from imiptools.dates import get_datetime, get_start_of_day, to_timezone +class Period: + + "A basic period abstraction." + + def __init__(self, start, end=None): + self.start = start + self.end = end + + def as_tuple(self): + return self.start, self.end + + def __hash__(self): + return hash((self.start, self.end)) + + def __cmp__(self, other): + if isinstance(other, Period): + return cmp((self.start, self.end), (other.start, other.end)) + else: + return 1 + + def get_key(self): + return self.start, self.end + + def __repr__(self): + return "Period(%r, %r)" % (self.start, self.end) + +class FreeBusyPeriod(Period): + + "A free/busy record abstraction." + + def __init__(self, start, end=None, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None): + Period.__init__(self, start, end) + self.uid = uid + self.transp = transp + self.recurrenceid = recurrenceid + self.summary = summary + self.organiser = organiser + + def as_tuple(self): + return self.start, self.end, self.uid, self.transp, self.recurrenceid, self.summary, self.organiser + + def __hash__(self): + return hash((self.start, self.end, self.uid)) + + def __cmp__(self, other): + if isinstance(other, FreeBusyPeriod): + return cmp((self.start, self.end, self.uid), (other.start, other.end, other.uid)) + else: + return Period.__cmp__(self, other) + + def get_key(self): + return self.uid, self.recurrenceid, self.start + + def __repr__(self): + return "FreeBusyPeriod(%r, %r, %r, %r, %r, %r, %r)" % ( + self.start, self.end, self.uid, self.transp, self.recurrenceid, + self.summary, self.organiser) + # Time management with datetime strings in the UTC time zone. def can_schedule(freebusy, periods, uid, recurrenceid): @@ -33,8 +91,7 @@ """ for conflict in have_conflict(freebusy, periods, True): - start, end, found_uid, found_transp, found_recurrenceid = conflict[:5] - if found_uid != uid and found_recurrenceid != recurrenceid: + if conflict.uid != uid and conflict.recurrenceid != recurrenceid: return False return True @@ -47,12 +104,12 @@ set to a true value. """ - conflicts = [] - for start, end in periods: - overlapping = period_overlaps(freebusy, (start, end), get_conflicts) + conflicts = set() + for p in periods: + overlapping = period_overlaps(freebusy, p, get_conflicts) if overlapping: if get_conflicts: - conflicts += overlapping + conflicts.update(overlapping) else: return True @@ -76,8 +133,8 @@ i = 0 while i < len(freebusy): - t = freebusy[i] - if len(t) >= 5 and t[2] == uid and t[4] == recurrenceid: + fb = freebusy[i] + if fb.uid == uid and fb.recurrenceid == recurrenceid: del freebusy[i] else: i += 1 @@ -94,10 +151,10 @@ i = 0 while i < len(freebusy): - t = freebusy[i] - if len(t) >= 5 and t[2] == uid and t[4] and ( + fb = freebusy[i] + if fb.uid == uid and fb.recurrenceid and ( recurrenceids is None or - recurrenceids is not None and t[4] not in recurrenceids + recurrenceids is not None and fb.recurrenceid not in recurrenceids ): del freebusy[i] else: @@ -112,18 +169,18 @@ as a reference to the originally-defined occurrence. """ - found = bisect_left(freebusy, (recurrenceid,)) + found = bisect_left(freebusy, Period(recurrenceid)) while found < len(freebusy): - start, end, _uid, transp, _recurrenceid = freebusy[found][:5] + fb = freebusy[found] # Stop looking if the start no longer matches the recurrence identifier. - if start != recurrenceid: + if fb.start != recurrenceid: return # If the period belongs to the parent object, remove it and return. - if not _recurrenceid and uid == _uid: + if not fb.recurrenceid and uid == fb.uid: del freebusy[found] break @@ -138,28 +195,25 @@ 'period'. """ - dtstart, dtend = period[:2] - # Find the range of periods potentially overlapping the period in the # free/busy collection. - last = bisect_right(freebusy, (dtend,)) + last = bisect_right(freebusy, Period(period.end)) startpoints = freebusy[:last] # Find the range of periods potentially overlapping the period in a version # of the free/busy collection sorted according to end datetimes. - endpoints = [((fb[1], fb[0]) + fb[2:]) for fb in startpoints] + endpoints = [(fb.end, fb.start, fb) for fb in startpoints] endpoints.sort() - first = bisect_left(endpoints, (dtstart,)) + first = bisect_left(endpoints, (period.start,)) endpoints = endpoints[first:] overlapping = set() - for fb in endpoints: - end, start = fb[:2] - if end > dtstart and start < dtend: - overlapping.add((start, end) + fb[2:]) + for end, start, fb in endpoints: + if end > period.start and start < period.end: + overlapping.add(fb) overlapping = list(overlapping) overlapping.sort() @@ -207,55 +261,49 @@ "Convert 'periods' to use datetime objects employing the given 'tzid'." - l = [] - - for t in periods: - start, end = t[:2] + for p in periods: # NOTE: This only really works if the datetimes are UTC already. # NOTE: Since the periods should originate from the free/busy data, # NOTE: and since that data should employ UTC times, this should not be # NOTE: an immediate problem. - start = get_datetime(start) - end = get_datetime(end) + start = get_datetime(p.start) + end = get_datetime(p.end) start = isinstance(start, datetime) and to_timezone(start, tzid) or get_start_of_day(start, tzid) end = isinstance(end, datetime) and to_timezone(end, tzid) or get_start_of_day(end, tzid) - l.append((start, end) + tuple(t[2:])) - - return l + p.start = start + p.end = end def get_scale(periods): """ - Return an ordered time scale from the given list 'periods', with the first - two elements of each tuple being start and end times. + Return an ordered time scale from the given list of 'periods'. The given 'tzid' is used to make sure that the times are defined according to the chosen time zone. The returned scale is a mapping from time to (starting, ending) tuples, - where starting and ending are collections of tuples from 'periods'. + where starting and ending are collections of periods. """ scale = {} - for t in periods: - start, end = t[:2] + for p in periods: # Add a point and this event to the starting list. - if not scale.has_key(start): - scale[start] = [], [] - scale[start][0].append(t) + if not scale.has_key(p.start): + scale[p.start] = [], [] + scale[p.start][0].append(p) # Add a point and this event to the ending list. - if not scale.has_key(end): - scale[end] = [], [] - scale[end][1].append(t) + if not scale.has_key(p.end): + scale[p.end] = [], [] + scale[p.end][1].append(p) return scale @@ -307,8 +355,7 @@ Return an ordered list of time slots from the given 'scale'. Each slot is a tuple containing details of a point in time for the start of - the slot, together with a list of parallel event tuples, each tuple - containing the original details of an event. + the slot, together with a list of parallel event periods. Each point in time is described as a Point representing the actual point in time together with an indicator of the nature of the point in time (as a @@ -473,41 +520,15 @@ spans = {} for _point, active in slots: - for t in active: - if t and len(t) >= 2: - start, end, uid, recurrenceid, summary, organiser, key = get_freebusy_details(t) - - start_slot = bisect_left(points, start) - end_slot = bisect_left(points, end) + for p in active: + if p: + key = p.get_key() + start_slot = bisect_left(points, p.start) + end_slot = bisect_left(points, p.end) spans[key] = end_slot - start_slot return spans -def get_freebusy_details(t): - - """ - Return a tuple of the form (start, end, uid, recurrenceid, summary, - organiser, key) from 't'. - """ - - # Handle both complete free/busy details... - - if len(t) >= 7: - start, end, uid, transp, recurrenceid, summary, organiser = t[:7] - key = uid, recurrenceid, start - - # ...and published details without specific event details. - - else: - start, end = t[:2] - uid = None - recurrenceid = None - summary = None - organiser = None - key = (start, end) - - return start, end, uid, recurrenceid, summary, organiser, key - def update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser): """ @@ -517,7 +538,7 @@ remove_period(freebusy, uid, recurrenceid) - for start, end in periods: - insert_period(freebusy, (start, end, uid, transp, recurrenceid, summary, organiser)) + for p in periods: + insert_period(freebusy, FreeBusyPeriod(p.start, p.end, uid, transp, recurrenceid, summary, organiser)) # vim: tabstop=4 expandtab shiftwidth=4 diff -r c0775c5083ff -r 00fcdf47658c imipweb/calendar.py --- a/imipweb/calendar.py Sat Mar 28 00:52:14 2015 +0100 +++ b/imipweb/calendar.py Sat Mar 28 00:55:12 2015 +0100 @@ -26,9 +26,8 @@ get_start_of_next_day, get_timestamp, ends_on_same_day, \ to_timezone from imiptools.period import add_day_start_points, add_empty_days, add_slots, \ - convert_periods, get_freebusy_details, \ - get_scale, get_slots, get_spans, partition_by_day, \ - Point + convert_periods, get_scale, get_slots, get_spans, \ + partition_by_day, Point from imipweb.resource import Resource class CalendarPage(Resource): @@ -318,7 +317,7 @@ # Obtain time point information for each group of periods. for periods in period_groups: - periods = convert_periods(periods, tzid) + convert_periods(periods, tzid) # Get the time scale with start and end points. @@ -599,8 +598,11 @@ # Show a column for each active period. - for t in active: - if t and len(t) >= 2: + for p in active: + + # The period can be None, meaning an empty column. + + if p: # Flush empty slots preceding this one. @@ -608,7 +610,7 @@ self._empty_slot(point, endpoint, empty) empty = 0 - start, end, uid, recurrenceid, summary, organiser, key = get_freebusy_details(t) + key = p.get_key() span = spans[key] # Produce a table cell only at the start of the period @@ -616,11 +618,11 @@ # Points defining the ends of instant events should # never define the start of new events. - if point.indicator == Point.PRINCIPAL and (point.point == start or continuation): + if point.indicator == Point.PRINCIPAL and (point.point == p.start or continuation): - has_continued = continuation and point.point != start - will_continue = not ends_on_same_day(point.point, end, tzid) - is_organiser = organiser == self.user + has_continued = continuation and point.point != p.start + will_continue = not ends_on_same_day(point.point, p.end, tzid) + is_organiser = p.organiser == self.user css = " ".join([ "event", @@ -633,9 +635,9 @@ # Need to only anchor the first period for a recurring # event. - html_id = "%s-%s-%s" % (group_type, uid, recurrenceid or "") + html_id = "%s-%s-%s" % (group_type, p.uid, p.recurrenceid or "") - if point.point == start and html_id not in self.html_ids: + if point.point == p.start and html_id not in self.html_ids: page.td(class_=css, rowspan=span, id=html_id) self.html_ids.add(html_id) else: @@ -644,10 +646,10 @@ # Only link to events if they are not being # updated by requests. - if not summary or (uid, recurrenceid) in self._get_requests() and group_type != "request": - page.span(summary or "(Participant is busy)") + if not p.summary or (p.uid, p.recurrenceid) in self._get_requests() and group_type != "request": + page.span(p.summary or "(Participant is busy)") else: - page.a(summary, href=self.link_to(uid, recurrenceid)) + page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) page.td.close() else: diff -r c0775c5083ff -r 00fcdf47658c imipweb/event.py --- a/imipweb/event.py Sat Mar 28 00:52:14 2015 +0100 +++ b/imipweb/event.py Sat Mar 28 00:55:12 2015 +0100 @@ -820,7 +820,7 @@ # Obtain the periods associated with the event in the user's time zone. - periods = obj.get_periods(self.get_tzid(), self.get_window_end(), origin=True) + periods = obj.get_periods(self.get_tzid(), self.get_window_end()) recurrenceids = self._get_recurrences(uid) if len(periods) == 1: @@ -834,19 +834,19 @@ # Determine whether any periods are explicitly created or are part of a # rule. - explicit_periods = filter(lambda t: t[2] != "RRULE", periods) + explicit_periods = filter(lambda p: p.origin != "RRULE", periods) # Show each recurrence in a separate table if editable. if is_organiser and explicit_periods: - for index, (start, end, origin) in enumerate(periods[1:]): + for index, p in enumerate(periods[1:]): # Isolate the controls from neighbouring tables. page.div() - self.show_object_datetime_controls(start, end, index) + self.show_object_datetime_controls(p.start, p.end, index) # NOTE: Need to customise the TH classes according to errors and # NOTE: index information. @@ -856,11 +856,11 @@ page.tbody() page.tr() page.th("Start", class_="objectheading start") - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True) + self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, True) page.tr.close() page.tr() page.th("End", class_="objectheading end") - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False) + self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, False) page.tr.close() page.tbody.close() page.table.close() @@ -883,10 +883,10 @@ # Show only subsequent periods if organiser, since the principal # period will be the start and end datetimes. - for index, (start, end, origin) in enumerate(is_organiser and periods[1:] or periods): + for index, p in enumerate(is_organiser and periods[1:] or periods): page.tr() - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True) - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False) + self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, True) + self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, False) page.tr.close() page.tbody.close() page.table.close() @@ -918,7 +918,8 @@ # Show any conflicts. - conflicts = [t for t in have_conflict(freebusy, periods, True) if t[2] != uid] + conflicts = list([p for p in have_conflict(freebusy, periods, True) if p.uid != uid]) + conflicts.sort() if conflicts: page.p("This event conflicts with others:") @@ -933,20 +934,19 @@ page.thead.close() page.tbody() - for t in conflicts: - start, end, found_uid, transp, found_recurrenceid, summary = t[:6] + for p in conflicts: # Provide details of any conflicting event. - start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long") - end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long") + start = self.format_datetime(to_timezone(get_datetime(p.start), tzid), "long") + end = self.format_datetime(to_timezone(get_datetime(p.end), tzid), "long") page.tr() # Show the event summary for the conflicting event. page.td() - page.a(summary, href=self.link_to(found_uid)) + page.a(p.summary, href=self.link_to(p.uid)) page.td.close() page.td(start) diff -r c0775c5083ff -r 00fcdf47658c imipweb/resource.py --- a/imipweb/resource.py Sat Mar 28 00:52:14 2015 +0100 +++ b/imipweb/resource.py Sat Mar 28 00:55:12 2015 +0100 @@ -23,7 +23,8 @@ from imiptools.client import Client from imiptools.data import get_uri, get_window_end, Object, uri_values from imiptools.dates import format_datetime, format_time -from imiptools.period import remove_period, remove_affected_period, update_freebusy +from imiptools.period import FreeBusyPeriod, \ + remove_period, remove_affected_period, update_freebusy from imipweb.env import CGIEnvironment import babel.dates import imip_store @@ -132,19 +133,20 @@ # Convert the periods to more substantial free/busy items. - for start, end in periods: + for p in periods: # Subtract any recurrences from the free/busy details of a # parent object. - if recurrenceid or start not in recurrenceids: - summary.append(( - start, end, uid, - obj.get_value("TRANSP"), - recurrenceid, - obj.get_value("SUMMARY"), - obj.get_value("ORGANIZER") - )) + if recurrenceid or p.start not in recurrenceids: + summary.append( + FreeBusyPeriod( + p.start, p.end, uid, + obj.get_value("TRANSP"), + recurrenceid, + obj.get_value("SUMMARY"), + obj.get_value("ORGANIZER") + )) return summary # Preference methods.