# HG changeset patch # User Paul Boddie # Date 1425165851 -3600 # Node ID 2d0ab2a511b93b7d0393cb2ba42c3848c13863ac # Parent d648e01b247e909b8a44eab2c19a0d2f0174cbfd Changed period generation to use an explicit end point, supporting inclusive end points in order to be able to test for the presence of particular recurrence instances. Added initial support for detaching specific instances from recurring events. diff -r d648e01b247e -r 2d0ab2a511b9 imip_manager.py --- a/imip_manager.py Sun Mar 01 00:20:17 2015 +0100 +++ b/imip_manager.py Sun Mar 01 00:24:11 2015 +0100 @@ -31,7 +31,8 @@ sys.path.append(LIBRARY_PATH) from imiptools.content import Handler -from imiptools.data import get_address, get_uri, make_freebusy, Object, to_part, \ +from imiptools.data import get_address, get_uri, get_window_end, make_freebusy, \ + Object, to_part, \ uri_dict, uri_item, uri_items, uri_values from imiptools.dates import format_datetime, format_time, get_date, get_datetime, \ get_datetime_item, get_default_timezone, \ @@ -126,6 +127,16 @@ prefs = self.get_preferences() return prefs.get("TZID") or get_default_timezone() + def get_window_size(self): + prefs = self.get_preferences() + try: + return int(prefs.get("window_size")) + except (TypeError, ValueError): + return 100 + + def get_window_end(self): + return get_window_end(self.get_tzid(), self.get_window_size()) + class ManagerHandler(Handler, Common): """ @@ -182,9 +193,8 @@ # newer details (since the outgoing handler updates this user's # free/busy details). - tzid = self.get_tzid() - - _update_freebusy(freebusy, self.obj.get_periods_for_freebusy(tzid), + _update_freebusy(freebusy, + self.obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()), self.obj.get_value("TRANSP") or "OPAQUE", self.uid, self.recurrenceid) @@ -345,7 +355,7 @@ for uid, recurrenceid in self._get_requests(): obj = self._get_object(uid, recurrenceid) if obj: - for start, end in obj.get_periods_for_freebusy(self.get_tzid()): + for start, end in obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()): summary.append((start, end, uid, obj.get_value("TRANSP"), recurrenceid)) return summary @@ -381,9 +391,9 @@ return self.store.remove_event(self.user, uid, recurrenceid) def update_freebusy(self, uid, recurrenceid, obj): - tzid = self.get_tzid() freebusy = self.store.get_freebusy(self.user) - update_freebusy(freebusy, self.user, obj.get_periods_for_freebusy(tzid), + update_freebusy(freebusy, self.user, + obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()), obj.get_value("TRANSP"), uid, recurrenceid, self.store) def remove_from_freebusy(self, uid, recurrenceid=None): @@ -1035,18 +1045,14 @@ page.p("This event modifies a recurring event.") - # Obtain the user's timezone. - - tzid = self.get_tzid() + # Obtain the periods associated with the event in the user's time zone. - window_size = 100 - - periods = obj.get_periods(self.get_tzid(), window_size) + periods = obj.get_periods(self.get_tzid(), self.get_window_end()) if len(periods) == 1: return - page.p("This event occurs on the following occasions within the next %d days:" % window_size) + page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) page.table(cellspacing=5, cellpadding=5, class_="conflicts") page.thead() diff -r d648e01b247e -r 2d0ab2a511b9 imiptools/content.py --- a/imiptools/content.py Sun Mar 01 00:20:17 2015 +0100 +++ b/imiptools/content.py Sun Mar 01 00:24:11 2015 +0100 @@ -24,7 +24,7 @@ from email.mime.text import MIMEText from imiptools.config import MANAGER_PATH, MANAGER_URL from imiptools.data import Object, parse_object, \ - get_address, get_uri, get_value, \ + get_address, get_uri, get_value, get_window_end, \ is_new_object, uri_dict, uri_item from imiptools.dates import format_datetime, to_timezone from imiptools.period import can_schedule, insert_period, remove_period, \ @@ -173,9 +173,12 @@ def remove_from_freebusy_for_other(self, freebusy, user, other): remove_from_freebusy_for_other(freebusy, user, other, self.uid, self.recurrenceid, self.store) - def update_freebusy(self, freebusy, attendee, periods): + def _update_freebusy(self, freebusy, attendee, periods, recurrenceid): update_freebusy(freebusy, attendee, periods, self.obj.get_value("TRANSP"), - self.uid, self.recurrenceid, self.store) + self.uid, recurrenceid, self.store) + + def update_freebusy(self, freebusy, attendee, periods): + self._update_freebusy(freebusy, attendee, periods, self.recurrenceid) def update_freebusy_from_participant(self, user, participant_item): @@ -190,9 +193,11 @@ if participant != user: freebusy = self.store.get_freebusy_for_other(user, participant) + window_end = get_window_end(tzid=None) + if participant_attr.get("PARTSTAT") != "DECLINED": update_freebusy_for_other(freebusy, user, participant, - self.obj.get_periods_for_freebusy(tzid=None), + self.obj.get_periods_for_freebusy(tzid=None, end=window_end), self.obj.get_value("TRANSP"), self.uid, self.recurrenceid, self.store) else: @@ -330,15 +335,32 @@ return senders + def _get_object(self, user, uid, recurrenceid): + + """ + Return the stored object for the given 'user', 'uid' and 'recurrenceid'. + """ + + fragment = self.store.get_event(user, uid, recurrenceid) + return fragment and Object(fragment) + def get_object(self, user): """ Return the stored object to which the current object refers for the - given 'user' and for the given 'objtype'. + given 'user'. """ - fragment = self.store.get_event(user, self.uid, self.recurrenceid) - return fragment and Object(fragment) + return self._get_object(user, self.uid, self.recurrenceid) + + def get_parent_object(self, user): + + """ + Return the parent object to which the current object refers for the + given 'user'. + """ + + return self._get_object(user, self.uid, None) def have_new_object(self, attendee, obj=None): diff -r d648e01b247e -r 2d0ab2a511b9 imiptools/data.py --- a/imiptools/data.py Sun Mar 01 00:20:17 2015 +0100 +++ b/imiptools/data.py Sun Mar 01 00:24:11 2015 +0100 @@ -92,11 +92,15 @@ # Computed results. - def get_periods(self, tzid, window_size=100): - return get_periods(self, tzid, window_size) + def has_recurrence(self, tzid, recurrence): + recurrences = [start for start, end in get_periods(self, tzid, recurrence, True)] + return recurrence in recurrences - def get_periods_for_freebusy(self, tzid, window_size=100): - periods = self.get_periods(tzid, window_size) + def get_periods(self, tzid, end): + return get_periods(self, tzid, end) + + def get_periods_for_freebusy(self, tzid, end): + periods = self.get_periods(tzid, end) return get_periods_for_freebusy(self, periods, tzid) # Construction and serialisation. @@ -339,11 +343,15 @@ # NOTE: Need to expose the 100 day window for recurring events in the # NOTE: configuration. -def get_periods(obj, tzid, window_size=100): +def get_window_end(tzid, window_size=100): + return to_timezone(datetime.now(), tzid) + timedelta(window_size) + +def get_periods(obj, tzid, window_end, inclusive=False): """ Return periods for the given object 'obj', confining materialised periods - to the given 'window_size' in days starting from the present moment. + 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. """ rrule = obj.get_value("RRULE") @@ -366,13 +374,11 @@ # for the agent, with instances outside that period being considered # unchecked. - window_end = to_timezone(datetime.now(), tzid) + timedelta(window_size) - selector = get_rule(dtstart, rrule) parameters = get_parameters(rrule) periods = [] - for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")): + 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)) diff -r d648e01b247e -r 2d0ab2a511b9 imiptools/handlers/person_outgoing.py --- a/imiptools/handlers/person_outgoing.py Sun Mar 01 00:20:17 2015 +0100 +++ b/imiptools/handlers/person_outgoing.py Sun Mar 01 00:24:11 2015 +0100 @@ -21,8 +21,8 @@ """ from imiptools.content import Handler -from imiptools.data import uri_dict, uri_item, uri_values -from imiptools.dates import get_default_timezone +from imiptools.data import get_window_end, uri_dict, uri_item, uri_values +from imiptools.dates import format_datetime, get_default_timezone, to_timezone from imiptools.profile import Preferences class PersonHandler(Handler): @@ -72,35 +72,75 @@ self.store.dequeue_request(identity, self.uid, self.recurrenceid) + # Interpretation of periods can depend on the time zone. + + preferences = Preferences(identity) + tzid = preferences.get("TZID") or get_default_timezone() + + # Where a recurring object is updated by a specific occurrence, the + # details of the recurring "parent" must be changed. + + if self.recurrenceid: + obj = self.get_parent_object(identity) + recurrence = self.obj.get_datetime("RECURRENCE-ID") + + if obj.has_recurrence(tzid, recurrence): + item = obj.get_item("EXDATE") + if item: + exdates, exdate_attr = item + if not isinstance(exdates, list): + exdates = [exdates] + else: + exdates, exdate_attr = [], {} + + # Convert the occurrence to the same time regime as the other + # exceptions. + + exdate_tzid = exdate_attr.get("TZID") + exdate = recurrence + if exdate_tzid: + exdate = to_timezone(exdate, exdate_tzid) + else: + exdate = to_timezone(exdate, "UTC") + + # Update the exceptions and store the modified parent event. + + exdates.append(format_datetime(exdate)) + obj["EXDATE"] = [(exdates, exdate_attr)] + + self.store.set_event(identity, self.uid, None, obj.to_node()) + # Update free/busy information. if update_freebusy: - # Use the stored event for non-organiser messages in case the reply - # is incomplete, as is seen when Claws sends a REPLY for an object - # originally employing recurrence information. + freebusy = self.store.get_freebusy(identity) - if not from_organiser: - obj = self.get_object(identity) - else: - obj = self.obj + # Use the stored event in case the reply is incomplete, as is seen + # when Claws sends a REPLY for an object originally employing + # recurrence information. Additionally, parent object details will + # also be consulted if available. - # Interpretation of periods can depend on the time zone. - - preferences = Preferences(identity) - tzid = preferences.get("TZID") or get_default_timezone() + obj = self.get_object(identity) # If newer than any old version, discard old details from the # free/busy record and check for suitability. - periods = obj.get_periods_for_freebusy(tzid) - freebusy = self.store.get_freebusy(identity) + periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid)) if attr.get("PARTSTAT") != "DECLINED": self.update_freebusy(freebusy, identity, periods) else: self.remove_from_freebusy(freebusy, identity) + # For any parent object, refresh the updated periods. + + if self.recurrenceid: + obj = self.get_parent_object(identity) + periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid)) + + self._update_freebusy(freebusy, identity, periods, None) + if self.publisher: self.publisher.set_freebusy(identity, freebusy)