# HG changeset patch # User Paul Boddie # Date 1508103038 -7200 # Node ID d8a75a959d5c66a34ec1b36627e9e093b15e63ac # Parent d4a1ce61c6261b301b1749086d9573bdc986d16a# Parent b4072888250fa416654b8b2ccc3771a6533132d0 Merged add-fallback-tzid-to-objects back into this branch. diff -r d4a1ce61c626 -r d8a75a959d5c imiptools/client.py --- a/imiptools/client.py Sun Oct 15 23:22:28 2017 +0200 +++ b/imiptools/client.py Sun Oct 15 23:30:38 2017 +0200 @@ -22,8 +22,8 @@ from collections import OrderedDict from datetime import datetime, timedelta from imiptools.config import settings -from imiptools.data import Object, check_delegation, get_address, get_uri, \ - get_main_period, get_recurrence_periods, \ +from imiptools.data import check_delegation, get_address, get_uri, \ + get_recurrence_periods, \ get_window_end, is_new_object, make_freebusy, \ make_uid, new_object, to_part, uri_dict, uri_item, \ uri_items, uri_parts, uri_values @@ -124,14 +124,17 @@ "Return the period window start as a datetime." prefs = self.get_preferences() - start = prefs and get_datetime(prefs.get("window_start"), {"TZID" : self.get_tzid()}) - return isinstance(start, datetime) and start or start and to_datetime(start, self.get_tzid()) + tzid = self.get_tzid() + start = prefs and get_datetime(prefs.get("window_start"), {"TZID" : tzid}) + return isinstance(start, datetime) and start or start and to_datetime(start, tzid) def get_window_end(self, size=None, start=None): "Return the period window end as a datetime." - return get_window_end(self.get_tzid(), size or self.get_window_size(), start or self.get_window_start()) + tzid = self.get_tzid() + return get_window_end(tzid, size or self.get_window_size(), + start or self.get_window_start()) def is_participating(self): @@ -248,8 +251,7 @@ def get_periods(self, obj, explicit_only=False, future_only=False): """ - Return periods for the given 'obj'. Interpretation of periods can depend - on the time zone, which is obtained for the current user. + Return periods for the given 'obj'. If 'explicit_only' is set to a true value, only explicit periods will be returned, not rule-based periods. @@ -258,7 +260,7 @@ returned, not all periods defined by an event starting in the past. """ - return obj.get_periods(self.get_tzid(), + return obj.get_periods( start=(future_only and self.get_window_start() or None), end=(not explicit_only and self.get_window_end() or None)) @@ -334,7 +336,7 @@ "Return the main period defined by 'obj'." - return obj.get_main_period(self.get_tzid()) + return obj.get_main_period() def get_recurrence_periods(self, obj): @@ -353,9 +355,16 @@ """ if section == "counters": - return self.store.get_counter(self.user, username, uid, recurrenceid) + obj = self.store.get_counter(self.user, username, uid, recurrenceid) else: - return self.store.get_event(self.user, uid, recurrenceid, section) + obj = self.store.get_event(self.user, uid, recurrenceid, section) + + # Set the fallback time zone. + + if obj: + obj.set_tzid(self.get_tzid()) + + return obj # Free/busy operations. @@ -492,11 +501,16 @@ self.sequence = obj and self.obj.get_value("SEQUENCE") self.dtstamp = obj and self.obj.get_value("DTSTAMP") + # Set the fallback time zone. + + if obj: + self.obj.set_tzid(self.get_tzid()) + def new_object(self, objtype): "Initialise a new object for the client with the given 'objtype'." - self.set_object(new_object(objtype, self.user, self.get_user_attributes())) + self.set_object(new_object(objtype, self.user, self.get_user_attributes(), self.get_tzid())) return self.obj def load_object(self, uid, recurrenceid): @@ -541,7 +555,7 @@ "Return whether the current object is a recurrence of its parent." parent = self.get_parent_object() - return parent and parent.has_recurrence(self.get_tzid(), self.obj.get_recurrenceid()) + return parent and parent.has_recurrence(self.obj.get_recurrenceid()) def get_recurrences(self, uid=None): @@ -1262,7 +1276,7 @@ invalid = [] - for period in self.obj.get_periods(self.get_tzid()): + for period in self.obj.get_periods(): errors = period.check_permitted(permitted_values) if errors: start_errors, end_errors = errors @@ -1275,7 +1289,7 @@ "Correct the object according to any scheduling constraints." permitted_values = self.get_permitted_values() - return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) + return permitted_values and self.obj.correct_object(permitted_values) def correct_period(self, period): @@ -1323,7 +1337,7 @@ "Get 'recurrenceid' in a form suitable for matching free/busy entries." - return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) + return self.obj.get_recurrence_start_point(recurrenceid) def remove_from_freebusy(self, freebusy, participant=None): diff -r d4a1ce61c626 -r d8a75a959d5c imiptools/data.py --- a/imiptools/data.py Sun Oct 15 23:22:28 2017 +0200 +++ b/imiptools/data.py Sun Oct 15 23:30:38 2017 +0200 @@ -44,13 +44,16 @@ "Access to calendar structures." - def __init__(self, fragment): + def __init__(self, fragment, tzid=None): """ - Initialise the object with the given 'fragment'. This must be a - dictionary mapping an object type (such as "VEVENT") to a tuple - containing the object details and attributes, each being a dictionary - itself. + Initialise the object with the given 'fragment'. The optional 'tzid' + sets the fallback time zone used to convert datetimes without time zone + information. + + The 'fragment' must be a dictionary mapping an object type (such as + "VEVENT") to a tuple containing the object details and attributes, + each being a dictionary itself. The result of parse_object can be processed to obtain a fragment by obtaining a collection of records for an object type. For example: @@ -73,6 +76,16 @@ """ self.objtype, (self.details, self.attr) = fragment.items()[0] + self.set_tzid(tzid) + + def set_tzid(self, tzid): + + """ + Set the fallback 'tzid' for interpreting datetimes without time zone + information. + """ + + self.tzid = tzid def get_uid(self): return self.get_value("UID") @@ -102,12 +115,13 @@ dt = to_timezone(to_datetime(dt, tzid), "UTC") return format_datetime(dt) - def get_recurrence_start_point(self, recurrenceid, tzid): + def get_recurrence_start_point(self, recurrenceid): """ 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. + the fallback time zone 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 @@ -119,21 +133,18 @@ recurrenceid = recurrenceid or self.get_recurrenceid() if recurrenceid: - return get_recurrence_start_point(recurrenceid, tzid) + return get_recurrence_start_point(recurrenceid, self.tzid) else: return None - def get_recurrence_start_points(self, recurrenceids, tzid): + def get_recurrence_start_points(self, recurrenceids): """ - Return start points for 'recurrenceids' using the fallback 'tzid' for + Return start points for 'recurrenceids' using the fallback time zone for identifiers with date representations. """ - points = [] - for recurrenceid in recurrenceids: - points.append(self.get_recurrence_start_point(recurrenceid, tzid)) - return points + return map(self.get_recurrence_start_point, recurrenceids) # Structure access. @@ -149,7 +160,7 @@ l.append((obj.details, obj.attr)) def copy(self): - return Object(self.to_dict()) + return Object(self.to_dict(), self.tzid) def get_items(self, name, all=True): return get_items(self.details, name, all) @@ -169,17 +180,18 @@ def set_value(self, name, value, attr=None): self.details[name] = [(value, attr or {})] - def get_utc_datetime(self, name, date_tzid=None): - return get_utc_datetime(self.details, name, date_tzid) + def get_utc_datetime(self, name): + return get_utc_datetime(self.details, name, self.tzid) - def get_date_value_items(self, name, tzid=None): - return get_date_value_items(self.details, name, tzid) + def get_date_value_items(self, name): + return get_date_value_items(self.details, name, self.tzid) - def get_date_value_item_periods(self, name, tzid=None): - return get_date_value_item_periods(self.details, name, self.get_main_period(tzid).get_duration(), tzid) + def get_date_value_item_periods(self, name): + return get_date_value_item_periods(self.details, name, + self.get_main_period().get_duration(), self.tzid) - def get_period_values(self, name, tzid=None): - return get_period_values(self.details, name, tzid) + def get_period_values(self, name): + return get_period_values(self.details, name, self.tzid) def get_datetime(self, name): t = get_datetime_item(self.details, name) @@ -244,7 +256,7 @@ # Computed results. - def get_main_period(self, tzid=None): + def get_main_period(self): """ Return a period object corresponding to the main start-end period for @@ -252,7 +264,7 @@ """ (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items() - tzid = tzid or get_tzid(dtstart_attr, dtend_attr) + tzid = get_tzid(dtstart_attr, dtend_attr) or self.tzid return RecurringPeriod(dtstart, dtend, tzid, "DTSTART", dtstart_attr, dtend_attr) def get_main_period_items(self): @@ -275,12 +287,12 @@ return (dtstart, dtstart_attr), (dtend, dtend_attr) - def get_periods(self, tzid, start=None, end=None, inclusive=False): + def get_periods(self, start=None, end=None, inclusive=False): """ - Return periods defined by this object, employing the given 'tzid' where - no time zone information is defined, and limiting the collection to a - window of time with the given 'start' and 'end'. + Return periods defined by this object, employing the fallback time zone + where no time zone information is defined, and limiting the collection + to a window of time with the given 'start' and 'end'. If 'end' is omitted, only explicit recurrences and recurrences from explicitly-terminated rules will be returned. @@ -289,43 +301,45 @@ will be included. """ - return get_periods(self, tzid, start, end, inclusive) + return get_periods(self, start, end, inclusive) - def has_period(self, tzid, period): + def has_period(self, period): """ - Return whether this object, employing the given 'tzid' where no time - zone information is defined, has the given 'period'. + Return whether this object, employing the fallback time zone where no + time zone information is defined, has the given 'period'. """ - return period in self.get_periods(tzid, end=period.get_start_point(), inclusive=True) + return period in self.get_periods(end=period.get_start_point(), inclusive=True) - def has_recurrence(self, tzid, recurrenceid): + def has_recurrence(self, recurrenceid): """ - Return whether this object, employing the given 'tzid' where no time - zone information is defined, has the given 'recurrenceid'. + Return whether this object, employing the fallback time zone where no + time zone information is defined, has the given 'recurrenceid'. """ - start_point = self.get_recurrence_start_point(recurrenceid, tzid) - for p in self.get_periods(tzid, end=start_point, inclusive=True): + start_point = self.get_recurrence_start_point(recurrenceid) + + for p in self.get_periods(end=start_point, inclusive=True): if p.get_start_point() == start_point: return True + return False - def get_active_periods(self, recurrenceids, tzid, start=None, end=None): + def get_active_periods(self, recurrenceids, start=None, end=None): """ Return all periods specified by this object that are not replaced by - those defined by 'recurrenceids', using 'tzid' as a fallback time zone - to convert floating dates and datetimes, and using 'start' and 'end' to + 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. """ # Specific recurrences yield all specified periods. - periods = self.get_periods(tzid, start, end) + periods = self.get_periods(start, end) if self.get_recurrenceid(): return periods @@ -398,17 +412,13 @@ """ Return a time zone identifier used by the start or end datetimes, - potentially suitable for converting dates to datetimes. + potentially suitable for converting dates to datetimes. Where no + identifier is associated with the datetimes, provide any fallback time + zone identifier. """ - if not self.has_key("DTSTART"): - return None - dtstart, dtstart_attr = self.get_datetime_item("DTSTART") - if self.has_key("DTEND"): - dtend, dtend_attr = self.get_datetime_item("DTEND") - else: - dtend_attr = None - return get_tzid(dtstart_attr, dtend_attr) + (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items() + return get_tzid(dtstart_attr, dtend_attr) or self.tzid def is_shared(self): @@ -419,15 +429,16 @@ return self.get_value("SEQUENCE") is not None - def possibly_active_from(self, dt, tzid): + def possibly_active_from(self, dt): """ Return whether the object is possibly active from or after the given - datetime 'dt' using 'tzid' to convert any dates or floating datetimes. + datetime 'dt' using the fallback time zone to convert any dates or + floating datetimes. """ - dt = to_datetime(dt, tzid) - periods = self.get_periods(tzid) + dt = to_datetime(dt, self.tzid) + periods = self.get_periods() for p in periods: if p.get_end_point() > dt: @@ -457,16 +468,16 @@ # Modification methods. - def set_datetime(self, name, dt, tzid=None): + def set_datetime(self, name, dt): """ - Set a datetime for property 'name' using 'dt' and the optional fallback - 'tzid', returning whether an update has occurred. + Set a datetime for property 'name' using 'dt' and the fallback time zone + where necessary, returning whether an update has occurred. """ if dt: old_value = self.get_value(name) - self[name] = [get_item_from_datetime(dt, tzid)] + self[name] = [get_item_from_datetime(dt, self.tzid)] return format_datetime(dt) != old_value return False @@ -581,17 +592,14 @@ return set(old_exdates) != new_exdates - def correct_object(self, tzid, permitted_values): + def correct_object(self, permitted_values): - """ - Correct the object's period details using the given 'tzid' and - 'permitted_values'. - """ + "Correct the object's period details using the 'permitted_values'." corrected = set() rdates = [] - for period in self.get_periods(tzid): + for period in self.get_periods(): corrected_period = period.get_corrected(permitted_values) if corrected_period is period: @@ -674,11 +682,12 @@ return ("VFREEBUSY", {}, record) -def parse_calendar(f, encoding): +def parse_calendar(f, encoding, tzid=None): """ Parse the iTIP content from 'f' having the given 'encoding'. Return a - mapping from object types to collections of calendar objects. + mapping from object types to collections of calendar objects. If 'tzid' is + specified, use it to set the fallback time zone on all returned objects. """ cal = parse_object(f, encoding, "VCALENDAR") @@ -687,7 +696,7 @@ for objtype, values in cal.items(): d[objtype] = l = [] for value in values: - l.append(Object({objtype : value})) + l.append(Object({objtype : value}, tzid)) return d @@ -767,12 +776,12 @@ finally: out.close() -def new_object(object_type, organiser=None, organiser_attr=None): +def new_object(object_type, organiser=None, organiser_attr=None, tzid=None): """ Make a new object of the given 'object_type' and optional 'organiser', with optional 'organiser_attr' describing any organiser identity in more - detail. + detail. An optional 'tzid' can also be provided. """ details = {} @@ -782,7 +791,7 @@ details["ORGANIZER"] = [(organiser, organiser_attr or {})] details["DTSTAMP"] = [(get_timestamp(), {})] - return Object({object_type : (details, {})}) + return Object({object_type : (details, {})}, tzid) def make_uid(user): @@ -1089,13 +1098,13 @@ return delegators -def get_periods(obj, tzid, start=None, end=None, inclusive=False): +def get_periods(obj, start=None, end=None, inclusive=False): """ - Return periods for the given object 'obj', employing the given 'tzid' where - no time zone information is available (for whole day events, for example), - confining materialised periods to after the given 'start' datetime and - before the given 'end' datetime. + Return periods for the given object 'obj', employing the object's fallback + time zone where no time zone information is available (for whole day events, + for example), confining materialised periods to after the given 'start' + datetime and before the given 'end' datetime. If 'end' is omitted, only explicit recurrences and recurrences from explicitly-terminated rules will be returned. @@ -1104,12 +1113,13 @@ will be included. """ + tzid = obj.get_tzid() rrule = obj.get_value("RRULE") parameters = rrule and get_parameters(rrule) # Use localised datetimes. - main_period = obj.get_main_period(tzid) + main_period = obj.get_main_period() dtstart = main_period.get_start() dtstart_attr = main_period.get_start_attr() @@ -1176,7 +1186,7 @@ # Add recurrence dates. - rdates = obj.get_date_value_item_periods("RDATE", tzid) + rdates = obj.get_date_value_item_periods("RDATE") if rdates: periods += rdates @@ -1186,7 +1196,7 @@ # Exclude exception dates. - exdates = obj.get_date_value_item_periods("EXDATE", tzid) + exdates = obj.get_date_value_item_periods("EXDATE") if exdates: for period in exdates: diff -r d4a1ce61c626 -r d8a75a959d5c imiptools/handlers/person_outgoing.py --- a/imiptools/handlers/person_outgoing.py Sun Oct 15 23:22:28 2017 +0200 +++ b/imiptools/handlers/person_outgoing.py Sun Oct 15 23:30:38 2017 +0200 @@ -4,7 +4,7 @@ Handlers for a person for whom scheduling is performed, inspecting outgoing messages to obtain scheduling done externally. -Copyright (C) 2014, 2015, 2016 Paul Boddie +Copyright (C) 2014, 2015, 2016, 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 @@ -51,6 +51,10 @@ else: self.user = self.get_sending_attendee() + # Update the fallback time zone information in the object. + + self.obj.set_tzid(self.get_tzid()) + def _add(self): "Add a recurrence for the current object." diff -r d4a1ce61c626 -r d8a75a959d5c imipweb/event.py --- a/imipweb/event.py Sun Oct 15 23:22:28 2017 +0200 +++ b/imipweb/event.py Sun Oct 15 23:30:38 2017 +0200 @@ -690,7 +690,7 @@ participant_attr = attendee_map.get(participant) partstat = participant_attr and participant_attr.get("PARTSTAT") - recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid) + recurrences = self.obj.get_recurrence_start_points(recurrenceids) for p in freebusy.have_conflict(periods, True): if not self.recurrenceid and p.is_replaced(recurrences): diff -r d4a1ce61c626 -r d8a75a959d5c imipweb/resource.py --- a/imipweb/resource.py Sun Oct 15 23:22:28 2017 +0200 +++ b/imipweb/resource.py Sun Oct 15 23:30:38 2017 +0200 @@ -179,7 +179,7 @@ # Obtain only active periods, not those replaced by redefined # recurrences, converting to free/busy periods. - for p in obj.get_active_periods(recurrenceids, self.get_tzid(), + for p in obj.get_active_periods(recurrenceids, start=view_period.get_start(), end=view_period.get_end()): summary.append(obj.get_freebusy_period(p)) diff -r d4a1ce61c626 -r d8a75a959d5c tests/test_handle.py --- a/tests/test_handle.py Sun Oct 15 23:22:28 2017 +0200 +++ b/tests/test_handle.py Sun Oct 15 23:30:38 2017 +0200 @@ -75,7 +75,7 @@ # the event. if action == "counter" or have_new_recurrence: - period = self.obj.get_main_period(self.get_tzid()) + period = self.obj.get_main_period() # Use the existing or configured time zone for the specified # datetimes. diff -r d4a1ce61c626 -r d8a75a959d5c tools/invite.py --- a/tools/invite.py Sun Oct 15 23:22:28 2017 +0200 +++ b/tools/invite.py Sun Oct 15 23:30:38 2017 +0200 @@ -44,11 +44,18 @@ if not recipients: raise ValueError("Recipients must be specified.") + # Obtain a timezone. + + if len(tzids) > 1: + raise ValueError("Only one timezone identifier should be given.") + + tzid = tzids and tzids[0] or get_default_timezone() + organiser = organisers[0] # Create an event for the calendar with the organiser and attendee details. - e = new_object("VEVENT", organiser) + e = new_object("VEVENT", organiser, tzid=tzid) attendees = [] @@ -60,13 +67,6 @@ e["ATTENDEE"] = attendees - # Obtain a timezone. - - if len(tzids) > 1: - raise ValueError("Only one timezone identifier should be given.") - - tzid = tzids and tzids[0] or get_default_timezone() - # Obtain the event periods converting them to datetimes. if not from_datetimes: diff -r d4a1ce61c626 -r d8a75a959d5c tools/make_freebusy.py --- a/tools/make_freebusy.py Sun Oct 15 23:22:28 2017 +0200 +++ b/tools/make_freebusy.py Sun Oct 15 23:30:38 2017 +0200 @@ -146,7 +146,7 @@ # Add each active period to the collection. - for p in obj.get_active_periods(recurrenceids, tzid, + for p in obj.get_active_periods(recurrenceids, start=window_start, end=window_end): # Obtain a suitable period object. @@ -178,7 +178,7 @@ # Update the list of objects providing periods on future occasions. if participant is None or storage is journal: - providers += [obj for obj in objs if obj.possibly_active_from(window_end, tzid)] + providers += [obj for obj in objs if obj.possibly_active_from(window_end)] # Alternatively, just write the collection to standard output.