# HG changeset patch # User Paul Boddie # Date 1425243609 -3600 # Node ID c591f1c62df621a66d75e9ed6c416aef35568539 # Parent 2d0ab2a511b93b7d0393cb2ba42c3848c13863ac Extended support for recurrence modifications to the other handlers, consolidating support in the Handler base class and simplifying free/busy operations in Handler and in the period module. Fixed the manager to use updated APIs. diff -r 2d0ab2a511b9 -r c591f1c62df6 imip_manager.py --- a/imip_manager.py Sun Mar 01 00:24:11 2015 +0100 +++ b/imip_manager.py Sun Mar 01 22:00:09 2015 +0100 @@ -42,8 +42,7 @@ from imiptools.period import add_day_start_points, add_empty_days, add_slots, \ convert_periods, get_freebusy_details, \ get_scale, have_conflict, get_slots, get_spans, \ - partition_by_day, remove_from_freebusy, update_freebusy, \ - _update_freebusy + partition_by_day, remove_period, update_freebusy from imiptools.profile import Preferences import imip_store import markup @@ -137,7 +136,7 @@ def get_window_end(self): return get_window_end(self.get_tzid(), self.get_window_size()) -class ManagerHandler(Handler, Common): +class ManagerHandler(Common, Handler): """ A content handler for use by the manager, as opposed to operating within the @@ -193,7 +192,7 @@ # newer details (since the outgoing handler updates this user's # free/busy details). - _update_freebusy(freebusy, + 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) @@ -392,13 +391,15 @@ def update_freebusy(self, uid, recurrenceid, obj): freebusy = self.store.get_freebusy(self.user) - update_freebusy(freebusy, self.user, + update_freebusy(freebusy, obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()), - obj.get_value("TRANSP"), uid, recurrenceid, self.store) + obj.get_value("TRANSP"), uid, recurrenceid) + self.store.set_freebusy(self.user, freebusy) def remove_from_freebusy(self, uid, recurrenceid=None): freebusy = self.store.get_freebusy(self.user) - remove_from_freebusy(freebusy, self.user, uid, recurrenceid, self.store) + remove_period(freebusy, uid, recurrenceid) + self.store.set_freebusy(self.user, freebusy) # Presentation methods. @@ -532,7 +533,7 @@ node = ("VEVENT", {}, record) - self.store.set_event(self.user, this_uid, node=node) + self.store.set_event(self.user, this_uid, None, node=node) self.store.queue_request(self.user, this_uid) # Redirect to the object (or the first of the objects), where instead of @@ -647,7 +648,7 @@ # Save single user events. elif save: - self.store.set_event(self.user, uid, node=obj.to_node()) + self.store.set_event(self.user, uid, None, node=obj.to_node()) self.update_freebusy(uid, None, obj=obj) self.remove_request(uid) diff -r 2d0ab2a511b9 -r c591f1c62df6 imiptools/content.py --- a/imiptools/content.py Sun Mar 01 00:24:11 2015 +0100 +++ b/imiptools/content.py Sun Mar 01 22:00:09 2015 +0100 @@ -26,11 +26,10 @@ from imiptools.data import Object, parse_object, \ 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.dates import format_datetime, get_default_timezone, to_timezone from imiptools.period import can_schedule, insert_period, remove_period, \ - remove_from_freebusy, \ - remove_from_freebusy_for_other, \ - update_freebusy, update_freebusy_for_other + remove_affected_period, update_freebusy +from imiptools.profile import Preferences from socket import gethostname import imip_store @@ -165,43 +164,65 @@ def get_outgoing_methods(self): return self.outgoing_methods - # Access to calendar structures and other data. + # Convenience methods for modifying free/busy collections. + + def remove_from_freebusy(self, freebusy): - def remove_from_freebusy(self, freebusy, attendee): - remove_from_freebusy(freebusy, attendee, self.uid, self.recurrenceid, self.store) + "Remove this event from the given 'freebusy' collection." + + remove_period(freebusy, self.uid, self.recurrenceid) + + def _update_freebusy(self, freebusy, periods, recurrenceid): - def remove_from_freebusy_for_other(self, freebusy, user, other): - remove_from_freebusy_for_other(freebusy, user, other, self.uid, self.recurrenceid, self.store) + """ + Update the 'freebusy' collection with the given 'periods', indicating an + explicit 'recurrenceid' to affect either a recurrence or the parent + event. + """ + + update_freebusy(freebusy, periods, self.obj.get_value("TRANSP"), + self.uid, recurrenceid) - def _update_freebusy(self, freebusy, attendee, periods, recurrenceid): - update_freebusy(freebusy, attendee, periods, self.obj.get_value("TRANSP"), - self.uid, recurrenceid, self.store) + def update_freebusy(self, freebusy, periods): - def update_freebusy(self, freebusy, attendee, periods): - self._update_freebusy(freebusy, attendee, periods, self.recurrenceid) + """ + Update the 'freebusy' collection for this event with the given + 'periods'. + """ + + self._update_freebusy(freebusy, periods, self.recurrenceid) + + # Convenience methods for updating stored free/busy information. def update_freebusy_from_participant(self, user, participant_item): """ For the given 'user', record the free/busy information for the - 'participant_item' (a value plus attributes), using the 'tzid' to define - period information. + 'participant_item' (a value plus attributes). """ participant, participant_attr = participant_item - if participant != user: - freebusy = self.store.get_freebusy_for_other(user, participant) + if participant == user: + return - window_end = get_window_end(tzid=None) + freebusy = self.store.get_freebusy_for_other(user, participant) + tzid = self.get_tzid(user) + window_end = get_window_end(tzid) - if participant_attr.get("PARTSTAT") != "DECLINED": - update_freebusy_for_other(freebusy, user, participant, - self.obj.get_periods_for_freebusy(tzid=None, end=window_end), - self.obj.get_value("TRANSP"), - self.uid, self.recurrenceid, self.store) - else: - self.remove_from_freebusy_for_other(freebusy, user, participant) + if participant_attr.get("PARTSTAT") != "DECLINED": + self.update_freebusy(freebusy, + self.obj.get_periods_for_freebusy(tzid, window_end) + ) + else: + self.remove_from_freebusy(freebusy) + + # Remove any specific recurrence from parent free/busy details. + + if self.recurrenceid: + remove_affected_period(freebusy, self.uid, self.recurrenceid) + + self.store.set_freebusy_for_other(user, freebusy, participant) def update_freebusy_from_organiser(self, attendee, organiser_item): @@ -219,6 +240,8 @@ for attendee_item in attendees.items(): self.update_freebusy_from_participant(organiser, attendee_item) + # Logic, filtering and access to calendar structures and other data. + def can_schedule(self, freebusy, periods): return can_schedule(freebusy, periods, self.uid, self.recurrenceid) @@ -360,7 +383,7 @@ given 'user'. """ - return self._get_object(user, self.uid, None) + return self.recurrenceid and self._get_object(user, self.uid, None) or None def have_new_object(self, attendee, obj=None): @@ -461,6 +484,60 @@ sequence = self.obj.get_value("SEQUENCE") or "0" self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] + def detach_recurrence(self, identity): + + "Detach the current object from its parent if it is a recurrence." + + # Where a recurring object is updated by a specific occurrence, the + # details of the recurring "parent" must be changed. + + obj = self.get_parent_object(identity) + if not obj: + return + + # The original recurrence is obtained, although the recurrence + # identifier could be converted back to a UTC datetime and used + # instead. + + recurrence = self.obj.get_datetime("RECURRENCE-ID") + if not obj.has_recurrence(self.get_tzid(identity), recurrence): + return + + # To detach the occurrence, the exceptions to the defined recurrence are + # modified. + + 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()) + + def get_tzid(self, identity): + + "Return the time regime applicable for the given 'identity'." + + preferences = Preferences(identity) + return preferences.get("TZID") or get_default_timezone() + # Handler registry. methods = { diff -r 2d0ab2a511b9 -r c591f1c62df6 imiptools/handlers/person.py --- a/imiptools/handlers/person.py Sun Mar 01 00:24:11 2015 +0100 +++ b/imiptools/handlers/person.py Sun Mar 01 22:00:09 2015 +0100 @@ -51,8 +51,11 @@ # Set the complete event if not an additional occurrence. - event = self.obj.to_node() - self.store.set_event(attendee, self.uid, self.recurrenceid, event) + self.store.set_event(attendee, self.uid, self.recurrenceid, self.obj.to_node()) + + # Detach any recurrence from its parent. + + self.detach_recurrence(attendee) # Queue any request. @@ -65,7 +68,9 @@ # information, so this is done here. freebusy = self.store.get_freebusy(attendee) - self.remove_from_freebusy(freebusy, attendee) + self.remove_from_freebusy(freebusy) + + self.store.set_freebusy(attendee, freebusy) if self.publisher: self.publisher.set_freebusy(attendee, freebusy) diff -r 2d0ab2a511b9 -r c591f1c62df6 imiptools/handlers/person_outgoing.py --- a/imiptools/handlers/person_outgoing.py Sun Mar 01 00:24:11 2015 +0100 +++ b/imiptools/handlers/person_outgoing.py Sun Mar 01 22:00:09 2015 +0100 @@ -22,8 +22,6 @@ from imiptools.content import Handler 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): @@ -59,10 +57,9 @@ if from_organiser: - # Set the complete event if not an additional occurrence. + # Set the complete event or an additional occurrence. - event = self.obj.to_node() - self.store.set_event(identity, self.uid, self.recurrenceid, event) + self.store.set_event(identity, self.uid, self.recurrenceid, self.obj.to_node()) else: organiser_item, attendees = self.require_organiser_and_attendees(from_organiser) @@ -72,43 +69,9 @@ 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") + # Detach any recurrence from its parent. - 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()) + self.detach_recurrence(identity) # Update free/busy information. @@ -116,6 +79,10 @@ freebusy = self.store.get_freebusy(identity) + # Interpretation of periods can depend on the time zone. + + tzid = self.get_tzid(identity) + # 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 @@ -129,17 +96,18 @@ periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid)) if attr.get("PARTSTAT") != "DECLINED": - self.update_freebusy(freebusy, identity, periods) + self.update_freebusy(freebusy, periods) else: - self.remove_from_freebusy(freebusy, identity) + self.remove_from_freebusy(freebusy) # For any parent object, refresh the updated periods. - if self.recurrenceid: - obj = self.get_parent_object(identity) + obj = self.get_parent_object(identity) + if obj: periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid)) + self._update_freebusy(freebusy, periods, None) - self._update_freebusy(freebusy, identity, periods, None) + self.store.set_freebusy(identity, freebusy) if self.publisher: self.publisher.set_freebusy(identity, freebusy) @@ -184,8 +152,7 @@ # Set the complete event if not an additional occurrence. - event = obj.to_node() - self.store.set_event(identity, self.uid, self.recurrenceid, event) + self.store.set_event(identity, self.uid, self.recurrenceid, obj.to_node()) # Remove any associated request. @@ -195,7 +162,9 @@ if update_freebusy: freebusy = self.store.get_freebusy(identity) - self.remove_from_freebusy(freebusy, identity) + self.remove_from_freebusy(freebusy) + + self.store.set_freebusy(identity, freebusy) if self.publisher: self.publisher.set_freebusy(identity, freebusy) diff -r 2d0ab2a511b9 -r c591f1c62df6 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Sun Mar 01 00:24:11 2015 +0100 +++ b/imiptools/handlers/resource.py Sun Mar 01 22:00:09 2015 +0100 @@ -20,10 +20,9 @@ """ from imiptools.content import Handler -from imiptools.data import get_address, get_uri, to_part +from imiptools.data import get_address, get_uri, get_window_end, to_part from imiptools.dates import get_default_timezone from imiptools.handlers.common import CommonFreebusy -from imiptools.profile import Preferences class ResourceHandler(Handler): @@ -60,13 +59,12 @@ # Interpretation of periods can depend on the time zone. - preferences = Preferences(attendee) - tzid = preferences.get("TZID") or get_default_timezone() + tzid = self.get_tzid(attendee) # If newer than any old version, discard old details from the # free/busy record and check for suitability. - periods = self.obj.get_periods_for_freebusy(tzid) + periods = self.obj.get_periods_for_freebusy(tzid, get_window_end(tzid)) freebusy = self.store.get_freebusy(attendee) scheduled = self.can_schedule(freebusy, periods) @@ -84,17 +82,30 @@ self.update_dtstamp() - # Set the complete event if not an additional occurrence. + # Set the complete event or an additional occurrence. event = self.obj.to_node() self.store.set_event(attendee, self.uid, self.recurrenceid, event) + # Detach any recurrence from its parent. + + self.detach_recurrence(attendee) + # Only update free/busy details if the event is scheduled. if scheduled: - self.update_freebusy(freebusy, attendee, periods) + self.update_freebusy(freebusy, periods) else: - self.remove_from_freebusy(freebusy, attendee) + self.remove_from_freebusy(freebusy) + + # For any parent object, refresh the updated periods. + + obj = self.get_parent_object(attendee) + if obj: + periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid)) + self._update_freebusy(freebusy, periods, None) + + self.store.set_freebusy(attendee, freebusy) if self.publisher: self.publisher.set_freebusy(attendee, freebusy) @@ -106,7 +117,9 @@ self.store.cancel_event(attendee, self.uid, self.recurrenceid) freebusy = self.store.get_freebusy(attendee) - self.remove_from_freebusy(freebusy, attendee) + self.remove_from_freebusy(freebusy) + + self.store.set_freebusy(attendee, freebusy) if self.publisher: self.publisher.set_freebusy(attendee, freebusy) diff -r 2d0ab2a511b9 -r c591f1c62df6 imiptools/period.py --- a/imiptools/period.py Sun Mar 01 00:24:11 2015 +0100 +++ b/imiptools/period.py Sun Mar 01 22:00:09 2015 +0100 @@ -73,6 +73,13 @@ else: i += 1 +def remove_affected_period(freebusy, uid, recurrenceid): + found = bisect_left(freebusy, (recurrenceid,)) + if found < len(freebusy): + start, end, _uid, transp, _recurrenceid = freebusy[found][:5] + if start == recurrenceid and recurrenceid != _recurrenceid and uid == _uid: + del freebusy[found] + def get_overlapping(freebusy, period): """ @@ -366,27 +373,7 @@ return start, end, uid, recurrenceid, key -def remove_from_freebusy(freebusy, attendee, uid, recurrenceid, store): - - """ - For the given 'attendee', remove periods from 'freebusy' that are associated - with 'uid' and 'recurrenceid' in the 'store'. - """ - - remove_period(freebusy, uid, recurrenceid) - store.set_freebusy(attendee, freebusy) - -def remove_from_freebusy_for_other(freebusy, user, other, uid, recurrenceid, store): - - """ - For the given 'user', remove for the 'other' party periods from 'freebusy' - that are associated with 'uid' and 'recurrenceid' in the 'store'. - """ - - remove_period(freebusy, uid, recurrenceid) - store.set_freebusy_for_other(user, freebusy, other) - -def _update_freebusy(freebusy, periods, transp, uid, recurrenceid): +def update_freebusy(freebusy, periods, transp, uid, recurrenceid): """ Update the free/busy details with the given 'periods', 'transp' setting and @@ -398,24 +385,4 @@ for start, end in periods: insert_period(freebusy, (start, end, uid, transp, recurrenceid)) -def update_freebusy(freebusy, attendee, periods, transp, uid, recurrenceid, store): - - """ - For the given 'attendee', update the free/busy details with the given - 'periods', 'transp' setting and 'uid' plus 'recurrenceid' in the 'store'. - """ - - _update_freebusy(freebusy, periods, transp, uid, recurrenceid) - store.set_freebusy(attendee, freebusy) - -def update_freebusy_for_other(freebusy, user, other, periods, transp, uid, recurrenceid, store): - - """ - For the given 'user', update the free/busy details of 'other' with the given - 'periods', 'transp' setting and 'uid' plus 'recurrenceid' in the 'store'. - """ - - _update_freebusy(freebusy, periods, transp, uid, recurrenceid) - store.set_freebusy_for_other(user, freebusy, other) - # vim: tabstop=4 expandtab shiftwidth=4