1.1 --- a/imip_manager.py Sun Mar 01 00:20:17 2015 +0100
1.2 +++ b/imip_manager.py Sun Mar 01 00:24:11 2015 +0100
1.3 @@ -31,7 +31,8 @@
1.4 sys.path.append(LIBRARY_PATH)
1.5
1.6 from imiptools.content import Handler
1.7 -from imiptools.data import get_address, get_uri, make_freebusy, Object, to_part, \
1.8 +from imiptools.data import get_address, get_uri, get_window_end, make_freebusy, \
1.9 + Object, to_part, \
1.10 uri_dict, uri_item, uri_items, uri_values
1.11 from imiptools.dates import format_datetime, format_time, get_date, get_datetime, \
1.12 get_datetime_item, get_default_timezone, \
1.13 @@ -126,6 +127,16 @@
1.14 prefs = self.get_preferences()
1.15 return prefs.get("TZID") or get_default_timezone()
1.16
1.17 + def get_window_size(self):
1.18 + prefs = self.get_preferences()
1.19 + try:
1.20 + return int(prefs.get("window_size"))
1.21 + except (TypeError, ValueError):
1.22 + return 100
1.23 +
1.24 + def get_window_end(self):
1.25 + return get_window_end(self.get_tzid(), self.get_window_size())
1.26 +
1.27 class ManagerHandler(Handler, Common):
1.28
1.29 """
1.30 @@ -182,9 +193,8 @@
1.31 # newer details (since the outgoing handler updates this user's
1.32 # free/busy details).
1.33
1.34 - tzid = self.get_tzid()
1.35 -
1.36 - _update_freebusy(freebusy, self.obj.get_periods_for_freebusy(tzid),
1.37 + _update_freebusy(freebusy,
1.38 + self.obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()),
1.39 self.obj.get_value("TRANSP") or "OPAQUE",
1.40 self.uid, self.recurrenceid)
1.41
1.42 @@ -345,7 +355,7 @@
1.43 for uid, recurrenceid in self._get_requests():
1.44 obj = self._get_object(uid, recurrenceid)
1.45 if obj:
1.46 - for start, end in obj.get_periods_for_freebusy(self.get_tzid()):
1.47 + for start, end in obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()):
1.48 summary.append((start, end, uid, obj.get_value("TRANSP"), recurrenceid))
1.49 return summary
1.50
1.51 @@ -381,9 +391,9 @@
1.52 return self.store.remove_event(self.user, uid, recurrenceid)
1.53
1.54 def update_freebusy(self, uid, recurrenceid, obj):
1.55 - tzid = self.get_tzid()
1.56 freebusy = self.store.get_freebusy(self.user)
1.57 - update_freebusy(freebusy, self.user, obj.get_periods_for_freebusy(tzid),
1.58 + update_freebusy(freebusy, self.user,
1.59 + obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()),
1.60 obj.get_value("TRANSP"), uid, recurrenceid, self.store)
1.61
1.62 def remove_from_freebusy(self, uid, recurrenceid=None):
1.63 @@ -1035,18 +1045,14 @@
1.64
1.65 page.p("This event modifies a recurring event.")
1.66
1.67 - # Obtain the user's timezone.
1.68 -
1.69 - tzid = self.get_tzid()
1.70 + # Obtain the periods associated with the event in the user's time zone.
1.71
1.72 - window_size = 100
1.73 -
1.74 - periods = obj.get_periods(self.get_tzid(), window_size)
1.75 + periods = obj.get_periods(self.get_tzid(), self.get_window_end())
1.76
1.77 if len(periods) == 1:
1.78 return
1.79
1.80 - page.p("This event occurs on the following occasions within the next %d days:" % window_size)
1.81 + page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
1.82
1.83 page.table(cellspacing=5, cellpadding=5, class_="conflicts")
1.84 page.thead()
2.1 --- a/imiptools/content.py Sun Mar 01 00:20:17 2015 +0100
2.2 +++ b/imiptools/content.py Sun Mar 01 00:24:11 2015 +0100
2.3 @@ -24,7 +24,7 @@
2.4 from email.mime.text import MIMEText
2.5 from imiptools.config import MANAGER_PATH, MANAGER_URL
2.6 from imiptools.data import Object, parse_object, \
2.7 - get_address, get_uri, get_value, \
2.8 + get_address, get_uri, get_value, get_window_end, \
2.9 is_new_object, uri_dict, uri_item
2.10 from imiptools.dates import format_datetime, to_timezone
2.11 from imiptools.period import can_schedule, insert_period, remove_period, \
2.12 @@ -173,9 +173,12 @@
2.13 def remove_from_freebusy_for_other(self, freebusy, user, other):
2.14 remove_from_freebusy_for_other(freebusy, user, other, self.uid, self.recurrenceid, self.store)
2.15
2.16 - def update_freebusy(self, freebusy, attendee, periods):
2.17 + def _update_freebusy(self, freebusy, attendee, periods, recurrenceid):
2.18 update_freebusy(freebusy, attendee, periods, self.obj.get_value("TRANSP"),
2.19 - self.uid, self.recurrenceid, self.store)
2.20 + self.uid, recurrenceid, self.store)
2.21 +
2.22 + def update_freebusy(self, freebusy, attendee, periods):
2.23 + self._update_freebusy(freebusy, attendee, periods, self.recurrenceid)
2.24
2.25 def update_freebusy_from_participant(self, user, participant_item):
2.26
2.27 @@ -190,9 +193,11 @@
2.28 if participant != user:
2.29 freebusy = self.store.get_freebusy_for_other(user, participant)
2.30
2.31 + window_end = get_window_end(tzid=None)
2.32 +
2.33 if participant_attr.get("PARTSTAT") != "DECLINED":
2.34 update_freebusy_for_other(freebusy, user, participant,
2.35 - self.obj.get_periods_for_freebusy(tzid=None),
2.36 + self.obj.get_periods_for_freebusy(tzid=None, end=window_end),
2.37 self.obj.get_value("TRANSP"),
2.38 self.uid, self.recurrenceid, self.store)
2.39 else:
2.40 @@ -330,15 +335,32 @@
2.41
2.42 return senders
2.43
2.44 + def _get_object(self, user, uid, recurrenceid):
2.45 +
2.46 + """
2.47 + Return the stored object for the given 'user', 'uid' and 'recurrenceid'.
2.48 + """
2.49 +
2.50 + fragment = self.store.get_event(user, uid, recurrenceid)
2.51 + return fragment and Object(fragment)
2.52 +
2.53 def get_object(self, user):
2.54
2.55 """
2.56 Return the stored object to which the current object refers for the
2.57 - given 'user' and for the given 'objtype'.
2.58 + given 'user'.
2.59 """
2.60
2.61 - fragment = self.store.get_event(user, self.uid, self.recurrenceid)
2.62 - return fragment and Object(fragment)
2.63 + return self._get_object(user, self.uid, self.recurrenceid)
2.64 +
2.65 + def get_parent_object(self, user):
2.66 +
2.67 + """
2.68 + Return the parent object to which the current object refers for the
2.69 + given 'user'.
2.70 + """
2.71 +
2.72 + return self._get_object(user, self.uid, None)
2.73
2.74 def have_new_object(self, attendee, obj=None):
2.75
3.1 --- a/imiptools/data.py Sun Mar 01 00:20:17 2015 +0100
3.2 +++ b/imiptools/data.py Sun Mar 01 00:24:11 2015 +0100
3.3 @@ -92,11 +92,15 @@
3.4
3.5 # Computed results.
3.6
3.7 - def get_periods(self, tzid, window_size=100):
3.8 - return get_periods(self, tzid, window_size)
3.9 + def has_recurrence(self, tzid, recurrence):
3.10 + recurrences = [start for start, end in get_periods(self, tzid, recurrence, True)]
3.11 + return recurrence in recurrences
3.12
3.13 - def get_periods_for_freebusy(self, tzid, window_size=100):
3.14 - periods = self.get_periods(tzid, window_size)
3.15 + def get_periods(self, tzid, end):
3.16 + return get_periods(self, tzid, end)
3.17 +
3.18 + def get_periods_for_freebusy(self, tzid, end):
3.19 + periods = self.get_periods(tzid, end)
3.20 return get_periods_for_freebusy(self, periods, tzid)
3.21
3.22 # Construction and serialisation.
3.23 @@ -339,11 +343,15 @@
3.24 # NOTE: Need to expose the 100 day window for recurring events in the
3.25 # NOTE: configuration.
3.26
3.27 -def get_periods(obj, tzid, window_size=100):
3.28 +def get_window_end(tzid, window_size=100):
3.29 + return to_timezone(datetime.now(), tzid) + timedelta(window_size)
3.30 +
3.31 +def get_periods(obj, tzid, window_end, inclusive=False):
3.32
3.33 """
3.34 Return periods for the given object 'obj', confining materialised periods
3.35 - to the given 'window_size' in days starting from the present moment.
3.36 + to before the given 'window_end' datetime. If 'inclusive' is set to a true
3.37 + value, any period occurring at the 'window_end' will be included.
3.38 """
3.39
3.40 rrule = obj.get_value("RRULE")
3.41 @@ -366,13 +374,11 @@
3.42 # for the agent, with instances outside that period being considered
3.43 # unchecked.
3.44
3.45 - window_end = to_timezone(datetime.now(), tzid) + timedelta(window_size)
3.46 -
3.47 selector = get_rule(dtstart, rrule)
3.48 parameters = get_parameters(rrule)
3.49 periods = []
3.50
3.51 - for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")):
3.52 + for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS"), inclusive):
3.53 start = to_timezone(datetime(*start), tzid)
3.54 end = start + duration
3.55 periods.append((start, end))
4.1 --- a/imiptools/handlers/person_outgoing.py Sun Mar 01 00:20:17 2015 +0100
4.2 +++ b/imiptools/handlers/person_outgoing.py Sun Mar 01 00:24:11 2015 +0100
4.3 @@ -21,8 +21,8 @@
4.4 """
4.5
4.6 from imiptools.content import Handler
4.7 -from imiptools.data import uri_dict, uri_item, uri_values
4.8 -from imiptools.dates import get_default_timezone
4.9 +from imiptools.data import get_window_end, uri_dict, uri_item, uri_values
4.10 +from imiptools.dates import format_datetime, get_default_timezone, to_timezone
4.11 from imiptools.profile import Preferences
4.12
4.13 class PersonHandler(Handler):
4.14 @@ -72,35 +72,75 @@
4.15
4.16 self.store.dequeue_request(identity, self.uid, self.recurrenceid)
4.17
4.18 + # Interpretation of periods can depend on the time zone.
4.19 +
4.20 + preferences = Preferences(identity)
4.21 + tzid = preferences.get("TZID") or get_default_timezone()
4.22 +
4.23 + # Where a recurring object is updated by a specific occurrence, the
4.24 + # details of the recurring "parent" must be changed.
4.25 +
4.26 + if self.recurrenceid:
4.27 + obj = self.get_parent_object(identity)
4.28 + recurrence = self.obj.get_datetime("RECURRENCE-ID")
4.29 +
4.30 + if obj.has_recurrence(tzid, recurrence):
4.31 + item = obj.get_item("EXDATE")
4.32 + if item:
4.33 + exdates, exdate_attr = item
4.34 + if not isinstance(exdates, list):
4.35 + exdates = [exdates]
4.36 + else:
4.37 + exdates, exdate_attr = [], {}
4.38 +
4.39 + # Convert the occurrence to the same time regime as the other
4.40 + # exceptions.
4.41 +
4.42 + exdate_tzid = exdate_attr.get("TZID")
4.43 + exdate = recurrence
4.44 + if exdate_tzid:
4.45 + exdate = to_timezone(exdate, exdate_tzid)
4.46 + else:
4.47 + exdate = to_timezone(exdate, "UTC")
4.48 +
4.49 + # Update the exceptions and store the modified parent event.
4.50 +
4.51 + exdates.append(format_datetime(exdate))
4.52 + obj["EXDATE"] = [(exdates, exdate_attr)]
4.53 +
4.54 + self.store.set_event(identity, self.uid, None, obj.to_node())
4.55 +
4.56 # Update free/busy information.
4.57
4.58 if update_freebusy:
4.59
4.60 - # Use the stored event for non-organiser messages in case the reply
4.61 - # is incomplete, as is seen when Claws sends a REPLY for an object
4.62 - # originally employing recurrence information.
4.63 + freebusy = self.store.get_freebusy(identity)
4.64
4.65 - if not from_organiser:
4.66 - obj = self.get_object(identity)
4.67 - else:
4.68 - obj = self.obj
4.69 + # Use the stored event in case the reply is incomplete, as is seen
4.70 + # when Claws sends a REPLY for an object originally employing
4.71 + # recurrence information. Additionally, parent object details will
4.72 + # also be consulted if available.
4.73
4.74 - # Interpretation of periods can depend on the time zone.
4.75 -
4.76 - preferences = Preferences(identity)
4.77 - tzid = preferences.get("TZID") or get_default_timezone()
4.78 + obj = self.get_object(identity)
4.79
4.80 # If newer than any old version, discard old details from the
4.81 # free/busy record and check for suitability.
4.82
4.83 - periods = obj.get_periods_for_freebusy(tzid)
4.84 - freebusy = self.store.get_freebusy(identity)
4.85 + periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid))
4.86
4.87 if attr.get("PARTSTAT") != "DECLINED":
4.88 self.update_freebusy(freebusy, identity, periods)
4.89 else:
4.90 self.remove_from_freebusy(freebusy, identity)
4.91
4.92 + # For any parent object, refresh the updated periods.
4.93 +
4.94 + if self.recurrenceid:
4.95 + obj = self.get_parent_object(identity)
4.96 + periods = obj.get_periods_for_freebusy(tzid, get_window_end(tzid))
4.97 +
4.98 + self._update_freebusy(freebusy, identity, periods, None)
4.99 +
4.100 if self.publisher:
4.101 self.publisher.set_freebusy(identity, freebusy)
4.102