# HG changeset patch # User Paul Boddie # Date 1438385703 -7200 # Node ID 23f998c9dacd6c185e71b579f9c3ebcfcf212be5 # Parent 2a4acfba1c128277f9a6aebabd89eb47f0e40a0c Changed normalised recurrence identifiers to employ UTC datetimes even for dates with time zone information, leaving only floating dates and datetimes to be converted using the user's time zone. Moved various period-modifying methods to the object abstraction. Tidied up period attribute definition from datetime information. Removed redundant recurrence identifier generation functions. Added a test of day event rescheduling employing the revised recurrence identifiers. diff -r 2a4acfba1c12 -r 23f998c9dacd imiptools/client.py --- a/imiptools/client.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imiptools/client.py Sat Aug 01 01:35:03 2015 +0200 @@ -24,8 +24,7 @@ is_new_object, make_freebusy, to_part, \ uri_dict, uri_items, uri_values from imiptools.dates import format_datetime, get_default_timezone, \ - get_recurrence_start_point, get_timestamp, \ - to_timezone + get_timestamp, to_timezone from imiptools.period import can_schedule, remove_period, \ remove_additional_periods, remove_affected_period, \ update_freebusy @@ -413,8 +412,7 @@ "Get 'recurrenceid' in a form suitable for matching free/busy entries." - tzid = self.obj.get_tzid() or self.get_tzid() - return get_recurrence_start_point(recurrenceid, tzid) + return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) def remove_from_freebusy(self, freebusy): diff -r 2a4acfba1c12 -r 23f998c9dacd imiptools/data.py --- a/imiptools/data.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imiptools/data.py Sat Aug 01 01:35:03 2015 +0200 @@ -26,7 +26,8 @@ get_datetime_item as get_item_from_datetime, \ get_datetime_tzid, \ get_duration, get_period, \ - get_tzid, to_timezone, to_utc_datetime + get_recurrence_start_point, \ + get_tzid, to_datetime, to_timezone, to_utc_datetime from imiptools.period import Period, RecurringPeriod, period_overlaps from vCalendar import iterwrite, parse, ParseError, to_dict, to_node from vRecurrence import get_parameters, get_rule @@ -51,12 +52,44 @@ """ Return the recurrence identifier, normalised to a UTC datetime if - specified as a datetime, converted to a date object otherwise. If no - recurrence identifier is present, None is returned. + specified as a datetime or date with accompanying time zone information, + maintained as a date or floating datetime otherwise. If no recurrence + identifier is present, None is returned. + + Note that this normalised form of the identifier may well not be the + same as the originally-specified identifier because that could have been + specified using an accompanying TZID attribute, whereas the normalised + form is effectively a converted datetime value. """ - recurrenceid = self.get_utc_datetime("RECURRENCE-ID") - return recurrenceid and format_datetime(recurrenceid) + if not self.has_key("RECURRENCE-ID"): + return None + dt, attr = self.get_datetime_item("RECURRENCE-ID") + 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, tzid): + + """ + Return the start point corresponding to the given 'recurrenceid', using + the fallback 'tzid' to define the specific point in time referenced by + the recurrence identifier if the identifier has a date representation. + + If 'recurrenceid' is given as None, this object's recurrence identifier + is used to obtain a start point, but if this object does not provide a + recurrence, None is returned. + + A start point is typically used to match free/busy periods which are + themselves defined in terms of UTC datetimes. + """ + + recurrenceid = recurrenceid or self.get_recurrenceid() + if recurrenceid: + return get_recurrence_start_point(recurrenceid, tzid) + else: + return None # Structure access. @@ -94,20 +127,6 @@ dt, attr = t return dt - def set_datetime(self, name, dt, tzid): - - """ - Set a datetime for property 'name' using 'dt' and 'tzid', returning - whether an update has occurred. - """ - - if dt: - old_value = self.get_value(name) - self[name] = [get_item_from_datetime(dt, tzid)] - return format_datetime(dt) != old_value - - return False - def get_datetime_item(self, name): return get_datetime_item(self.details, name) @@ -159,14 +178,6 @@ return get_periods(self, tzid, end) - def set_period(self, period): - - "Set the given 'period' as the main start and end." - - result = self.set_datetime("DTSTART", period.get_start(), period.start_attr().get("TZID")) - result = self.set_datetime("DTEND", period.get_end(), period.end_attr().get("TZID")) or result - return result - def get_tzid(self): """ @@ -189,6 +200,54 @@ return self.get_value("SEQUENCE") is not None + # Modification methods. + + def set_datetime(self, name, dt, tzid=None): + + """ + Set a datetime for property 'name' using 'dt' and the optional fallback + 'tzid', returning whether an update has occurred. + """ + + if dt: + old_value = self.get_value(name) + self[name] = [get_item_from_datetime(dt, tzid)] + return format_datetime(dt) != old_value + + return False + + def set_period(self, period): + + "Set the given 'period' as the main start and end." + + result = self.set_datetime("DTSTART", period.get_start()) + result = self.set_datetime("DTEND", period.get_end()) or result + return result + + def set_periods(self, periods): + + """ + Set the given 'periods' as recurrence date properties, replacing the + previous RDATE properties and ignoring any RRULE properties. + """ + + update = False + + old_values = self.get_values("RDATE") + new_rdates = [] + + if self.has_key("RDATE"): + del self["RDATE"] + + for p in periods: + if p.origin != "RRULE": + new_rdates.append(get_period_item(p.get_start(), p.get_end())) + + self["RDATE"] = new_rdates + + # NOTE: To do: calculate the update status. + return update + # Construction and serialisation. def make_calendar(nodes, method=None): diff -r 2a4acfba1c12 -r 23f998c9dacd imiptools/dates.py --- a/imiptools/dates.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imiptools/dates.py Sat Aug 01 01:35:03 2015 +0200 @@ -273,6 +273,15 @@ else: return None +def get_period_tzid(start, end): + + "Return the time zone identifier for 'start' and 'end' or None if unknown." + + if isinstance(start, datetime) or isinstance(end, datetime): + return get_datetime_tzid(start) or get_datetime_tzid(end) + else: + return None + def to_date(dt): "Return the date of 'dt'." @@ -375,8 +384,6 @@ else: return {"VALUE" : "DATE"} - return {} - def get_datetime_item(dt, tzid=None): """ @@ -392,11 +399,15 @@ attr = get_datetime_attributes(dt, tzid) return value, attr -def get_period_attributes(tzid=None): +def get_period_attributes(start, end, tzid=None): - "Return attributes for 'tzid'." + """ + Return attributes for the 'start' and 'end' datetime objects with 'tzid' + indicating the time zone if not otherwise defined. + """ attr = {"VALUE" : "PERIOD"} + tzid = get_period_tzid(start, end) or tzid if tzid: attr["TZID"] = tzid return attr @@ -408,17 +419,14 @@ 'tzid'. """ - start = start and to_timezone(start, tzid) - end = end and to_timezone(end, tzid) - - start_value = start and format_datetime(start) or None - end_value = end and format_datetime(end) or None - if start and end: - attr = get_period_attributes(tzid) + attr = get_period_attributes(start, end, tzid) + start_value = format_datetime(to_timezone(start, attr.get("TZID"))) + end_value = format_datetime(to_timezone(end, attr.get("TZID"))) return "%s/%s" % (start_value, end_value), attr elif start: attr = get_datetime_attributes(start, tzid) + start_value = format_datetime(to_timezone(start, attr.get("TZID"))) return start_value, attr else: return None, None @@ -442,7 +450,9 @@ """ Return 'recurrenceid' in a form suitable for comparison with period start - dates or datetimes. + dates or datetimes. The 'recurrenceid' should be an identifier normalised to + a UTC datetime or employing a date or floating datetime representation where + no time zone information was originally provided. """ return get_datetime(recurrenceid) @@ -452,26 +462,11 @@ """ Return 'recurrenceid' in a form suitable for comparison with free/busy start datetimes, using 'tzid' to convert recurrence identifiers that are dates. + The 'recurrenceid' should be an identifier normalised to a UTC datetime or + employing a date or floating datetime representation where no time zone + information was originally provided. """ return to_utc_datetime(get_datetime(recurrenceid), tzid) -def to_recurrence_start(recurrenceid): - - """ - Return 'recurrenceid' in a form suitable for use as an unambiguous - identifier. - """ - - return format_datetime(get_recurrence_start(recurrenceid)) - -def to_recurrence_start_point(recurrenceid, tzid): - - """ - Return 'recurrenceid' in a form suitable for use as an unambiguous - identifier, using 'tzid' to convert recurrence identifiers that are dates. - """ - - return format_datetime(get_recurrence_start_point(recurrenceid, tzid)) - # vim: tabstop=4 expandtab shiftwidth=4 diff -r 2a4acfba1c12 -r 23f998c9dacd imiptools/period.py --- a/imiptools/period.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imiptools/period.py Sat Aug 01 01:35:03 2015 +0200 @@ -189,7 +189,8 @@ """ Return whether 'period' refers to one of the 'recurrenceids', interpreted - using 'tzid' if necessary. + using 'tzid' if necessary. The 'recurrenceids' should be normalised to UTC + datetimes according to time zone information provided by their objects. """ for s in recurrenceids: @@ -200,8 +201,11 @@ def is_affected(period, recurrenceid, tzid): """ - Return whether 'period' refer to 'recurrenceid', interpreted using 'tzid' if - necessary. + Return whether 'period' refers to 'recurrenceid', interpreted using 'tzid' + if necessary. The 'recurrenceid' should be normalised to UTC datetimes + according to time zone information provided by their objects. Otherwise, + 'tzid' is used to convert any date or floating datetime representation to a + point in time. """ if not recurrenceid: diff -r 2a4acfba1c12 -r 23f998c9dacd imipweb/client.py --- a/imipweb/client.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imipweb/client.py Sat Aug 01 01:35:03 2015 +0200 @@ -127,7 +127,7 @@ for p in to_unschedule: if not p.origin: continue - obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), {})] + obj["RECURRENCE-ID"] = [p.get_start_item()] parts.append(obj.to_part("CANCEL")) # Send the updated event, along with a cancellation for each of the diff -r 2a4acfba1c12 -r 23f998c9dacd imipweb/event.py --- a/imipweb/event.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imipweb/event.py Sat Aug 01 01:35:03 2015 +0200 @@ -22,7 +22,7 @@ from datetime import date, timedelta from imiptools.data import get_uri, uri_dict, uri_values from imiptools.dates import format_datetime, get_datetime_item, \ - get_period_item, to_date, to_timezone + to_date, to_timezone from imiptools.mail import Messenger from imiptools.period import have_conflict from imipweb.data import EventPeriod, \ @@ -166,7 +166,7 @@ to_unschedule = self.get_removed_periods() obj.set_period(period) - self.set_periods_in_object(obj, periods) + obj.set_periods(periods) # Update summary. @@ -234,27 +234,6 @@ return None - def set_periods_in_object(self, obj, periods): - - "Set in the given 'obj' the given 'periods'." - - update = False - - old_values = obj.get_values("RDATE") - new_rdates = [] - - if obj.has_key("RDATE"): - del obj["RDATE"] - - for p in periods: - if p.origin != "RRULE": - new_rdates.append(get_period_item(p.get_start(), p.get_end(), p.get_tzid())) - - obj["RDATE"] = new_rdates - - # NOTE: To do: calculate the update status. - return update - def handle_main_period(self): "Return period details for the main start/end period in an event." diff -r 2a4acfba1c12 -r 23f998c9dacd imipweb/resource.py --- a/imipweb/resource.py Sat Aug 01 01:25:21 2015 +0200 +++ b/imipweb/resource.py Sat Aug 01 01:35:03 2015 +0200 @@ -214,10 +214,9 @@ # Subtract any recurrences from the free/busy details of a parent # object. - # NOTE: The time zone may need obtaining using the object details. for recurrenceid in self._get_recurrences(uid): - remove_affected_period(freebusy, uid, get_recurrence_start_point(recurrenceid, self.get_tzid())) + remove_affected_period(freebusy, uid, obj.get_recurrence_start_point(recurrenceid, self.get_tzid())) self.store.set_freebusy(self.user, freebusy) self.publish_freebusy(freebusy) diff -r 2a4acfba1c12 -r 23f998c9dacd tests/templates/event-request-recurring-reschedule-instance.txt --- a/tests/templates/event-request-recurring-reschedule-instance.txt Sat Aug 01 01:25:21 2015 +0200 +++ b/tests/templates/event-request-recurring-reschedule-instance.txt Sat Aug 01 01:35:03 2015 +0200 @@ -2,7 +2,7 @@ MIME-Version: 1.0 From: paul.boddie@example.com To: resource-room-confroom@example.com -Subject: Invitation! +Subject: Rescheduling! --===============0047278175== Content-Type: text/plain; charset="us-ascii" diff -r 2a4acfba1c12 -r 23f998c9dacd tests/test_resource_invitation_recurring_day.sh --- a/tests/test_resource_invitation_recurring_day.sh Sat Aug 01 01:25:21 2015 +0200 +++ b/tests/test_resource_invitation_recurring_day.sh Sat Aug 01 01:35:03 2015 +0200 @@ -43,3 +43,22 @@ && grep -q 'FREEBUSY;FBTYPE=BUSY:20141211T230000Z/20141212T230000Z' out2.tmp \ && echo "Success" \ || echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-day-reschedule-instance.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out3.tmp + + grep -q 'METHOD:REPLY' out3.tmp \ +&& grep -q 'ATTENDEE;PARTSTAT=ACCEPTED' out3.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out4.tmp + + grep -q 'METHOD:REPLY' out4.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141114T230000Z/20141115T230000Z' out4.tmp \ +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20141113T230000Z/20141114T230000Z' out4.tmp \ +&& echo "Success" \ +|| echo "Failed" diff -r 2a4acfba1c12 -r 23f998c9dacd tools/make_freebusy.py --- a/tools/make_freebusy.py Sat Aug 01 01:25:21 2015 +0200 +++ b/tools/make_freebusy.py Sat Aug 01 01:35:03 2015 +0200 @@ -22,7 +22,7 @@ """ from imiptools.data import get_window_end, Object -from imiptools.dates import format_datetime, get_default_timezone, to_recurrence_start +from imiptools.dates import get_default_timezone from imiptools.period import FreeBusyPeriod, is_replaced from imiptools.profile import Preferences from imip_store import FileStore, FilePublisher @@ -36,7 +36,6 @@ """ recurrenceid = obj.get_recurrenceid() - recurrenceids = [to_recurrence_start(r) for r in recurrenceids] for p in obj.get_periods(tzid, window_end): if recurrenceid or not is_replaced(p, recurrenceids, tzid):