# HG changeset patch # User Paul Boddie # Date 1508171853 -7200 # Node ID e71ec4159e8dd53952bf965ba3f222f00bebe59b # Parent d8a75a959d5c66a34ec1b36627e9e093b15e63ac Added support for modifications and cancellations in objects, moving the updated period computation into the object abstraction, changing the active period computation to incorporate modified periods and to use the updated period results. diff -r d8a75a959d5c -r e71ec4159e8d imiptools/client.py --- a/imiptools/client.py Sun Oct 15 23:30:38 2017 +0200 +++ b/imiptools/client.py Mon Oct 16 18:37:33 2017 +0200 @@ -264,73 +264,31 @@ start=(future_only and self.get_window_start() or None), end=(not explicit_only and self.get_window_end() or None)) - def get_updated_periods(self, obj): + def get_updated_periods(self, obj, explicit_only=False, future_only=False): """ Return the periods provided by 'obj' and associated recurrence instances. Each original period is returned in a tuple with a corresponding updated period which may be the same or which may be None if the period is cancelled. A list of these tuples is returned. + + If 'explicit_only' is set to a true value, only explicit periods will be + returned, not rule-based periods. + + If 'future_only' is set to a true value, only future periods will be + returned, not all periods defined by an event starting in the past. """ uid = obj.get_uid() - recurrenceid = obj.get_recurrenceid() - - updated = [] - - # Consider separate recurrences in isolation from the parent if - # specified. - - if recurrenceid: - for period in self.get_periods(obj): - updated.append((period, period)) - return updated - - # For parent events, identify retained and replaced periods. - - recurrenceids = self.get_recurrences(uid) - - for period in self.get_periods(obj): - recurrenceid = period.is_replaced(recurrenceids) - - # For parent event periods, obtain any replacement instead of the - # replaced period. - - if recurrenceid: - recurrence = self.get_stored_object(uid, recurrenceid) - periods = recurrence and self.get_periods(recurrence) - - # Active periods are obtained. - if periods: - - # Recurrence instances are assumed to provide only one - # period. - - replacement = periods[0] - - # Redefine the origin of periods replacing recurrences and - # not the main period, leaving DTSTART as the means of - # identifying the main period. - - if replacement.origin == "DTSTART" and \ - period.origin != "DTSTART": + if not obj.modifying: + obj.set_modifying(self.store.get_active_recurrences(self.user, uid)) + if not obj.cancelling: + obj.set_cancelling(self.store.get_cancelled_recurrences(self.user, uid)) - replacement.origin = "DTSTART-RECUR" - - updated.append((period, replacement)) - - # Cancelled periods yield None. - - else: - updated.append((period, None)) - - # Otherwise, retain the known period. - - else: - updated.append((period, period)) - - return updated + return obj.get_updated_periods( + start=(future_only and self.get_window_start() or None), + end=(not explicit_only and self.get_window_end() or None)) def get_main_period(self, obj): diff -r d8a75a959d5c -r e71ec4159e8d imiptools/data.py --- a/imiptools/data.py Sun Oct 15 23:30:38 2017 +0200 +++ b/imiptools/data.py Mon Oct 16 18:37:33 2017 +0200 @@ -78,6 +78,11 @@ self.objtype, (self.details, self.attr) = fragment.items()[0] self.set_tzid(tzid) + # Modify the object with separate recurrences. + + self.modifying = [] + self.cancelling = [] + def set_tzid(self, tzid): """ @@ -87,7 +92,34 @@ self.tzid = tzid + def set_modifying(self, modifying): + + """ + Set the 'modifying' objects affecting the periods provided by this + object. Such modifications can only be performed on a parent object, not + a specific recurrence object. + """ + + if not self.get_recurrenceid(): + self.modifying = modifying + + def set_cancelling(self, cancelling): + + """ + Set the 'cancelling' objects affecting the periods provided by this + object. Such cancellations can only be performed on a parent object, not + a specific recurrence object. + """ + + if not self.get_recurrenceid(): + self.cancelling = cancelling + + # Basic object identification. + def get_uid(self): + + "Return the universal identifier." + return self.get_value("UID") def get_recurrenceid(self): @@ -106,6 +138,7 @@ if not self.has_key("RECURRENCE-ID"): return None + dt, attr = self.get_datetime_item("RECURRENCE-ID") # Coerce any date to a UTC datetime if TZID was specified. @@ -113,6 +146,7 @@ tzid = attr.get("TZID") if tzid: dt = to_timezone(to_datetime(dt, tzid), "UTC") + return format_datetime(dt) def get_recurrence_start_point(self, recurrenceid): @@ -327,35 +361,87 @@ return False - def get_active_periods(self, recurrenceids, start=None, end=None): + def get_updated_periods(self, start=None, end=None): """ - Return all periods specified by this object that are not replaced by - those defined by 'recurrenceids', using the fallback time zone to - convert floating dates and datetimes, and using 'start' and 'end' to - respectively indicate the start and end of the time window within which - periods are considered. + Return pairs of periods specified by this object and any modifying or + cancelling objects, providing correspondences between the original + period definitions and those applying after modifications and + cancellations have been considered. + + The fallback time zone is used to convert floating dates and datetimes, + and 'start' and 'end' respectively indicate the start and end of any + time window within which periods are considered. """ # Specific recurrences yield all specified periods. - periods = self.get_periods(start, end) + original = self.get_periods(start, end) if self.get_recurrenceid(): - return periods + return original # Parent objects need to have their periods tested against redefined # recurrences. + modified = {} + + for obj in self.modifying: + periods = obj.get_periods(start, end) + if periods: + modified[obj.get_recurrenceid()] = periods[0] + + cancelled = set() + + for obj in self.cancelling: + cancelled.add(obj.get_recurrenceid()) + + updated = [] + + for p in original: + recurrenceid = p.is_replaced(modified.keys()) + + # Produce an original-to-modified correspondence, setting the origin + # to distinguish the period from the main period. + + if recurrenceid: + mp = modified.get(recurrenceid) + if mp.origin == "DTSTART" and p.origin != "DTSTART": + mp.origin = "DTSTART-RECUR" + updated.append((p, mp)) + break + + # Produce an original-to-null correspondence where cancellation has + # occurred. + + recurrenceid = p.is_replaced(cancelled) + + if recurrenceid: + updated.append((p, None)) + break + + # Produce an identity correspondence where no modification or + # cancellation has occurred. + + updated.append((p, p)) + + return updated + + def get_active_periods(self, start=None, end=None): + + """ + Return all periods specified by this object that are not replaced by + those defined by modifying or cancelling objects, using the fallback + time zone to convert floating dates and datetimes, and using 'start' and + 'end' to respectively indicate the start and end of the time window + within which periods are considered. + """ + active = [] - for p in periods: - - # Subtract any recurrences from the free/busy details of a - # parent object. - - if not p.is_replaced(recurrenceids): - active.append(p) + for old, new in self.get_updated_periods(start, end): + if new: + active.append(new) return active diff -r d8a75a959d5c -r e71ec4159e8d tests/internal/objects.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/internal/objects.py Mon Oct 16 18:37:33 2017 +0200 @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +""" +Test objects. + +Copyright (C) 2017 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from imiptools.data import parse_string, Object +from imiptools.dates import get_datetime + +# Define a parent object plus recurrence instances. + +parent_str = """\ +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com +DTSTAMP:20141009T182400Z +DTSTART;TZID=Europe/Oslo:20141010 +DTEND;TZID=Europe/Oslo:20141011 +RRULE:FREQ=MONTHLY;BYDAY=2FR;COUNT=3 +SUMMARY:Recurring event +UID:event4@example.com +END:VEVENT +""" + +modification_str = """\ +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com +DTSTAMP:20141009T182500Z +DTSTART;TZID=Europe/Oslo:20141115 +DTEND;TZID=Europe/Oslo:20141116 +SUMMARY:Recurring event +UID:event4@example.com +RECURRENCE-ID;TZID=Europe/Oslo:20141114 +SEQUENCE:2 +END:VEVENT +""" + +cancellation_str = """\ +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com +DTSTAMP:20141009T182500Z +DTSTART;TZID=Europe/Oslo:20141213 +DTEND;TZID=Europe/Oslo:20141214 +SUMMARY:Recurring event +UID:event4@example.com +RECURRENCE-ID;TZID=Europe/Oslo:20141213 +SEQUENCE:2 +END:VEVENT +""" + +# Parse the objects. + +parent = Object(parse_string(parent_str, "utf-8")) +modification = Object(parse_string(modification_str, "utf-8")) +cancellation = Object(parse_string(cancellation_str, "utf-8")) + +# Set the recurrence objects in the parent. + +parent.set_modifying([modification]) +parent.set_cancelling([cancellation]) + +# Obtain periods within a window of time. + +tzid = parent.get_tzid() +start = get_datetime("20141001T000000", {"TZID" : tzid}) +end = get_datetime("20151001", {"TZID" : tzid}) + +for p in parent.get_periods(start, end): + print p + +print "----" + +for old, new in parent.get_updated_periods(start, end): + print old + print new + print + +print "----" + +for p in parent.get_active_periods(start, end): + print p + +# vim: tabstop=4 expandtab shiftwidth=4