1.1 --- a/imiptools/client.py Sun Jul 26 02:01:24 2015 +0200
1.2 +++ b/imiptools/client.py Sun Jul 26 23:43:26 2015 +0200
1.3 @@ -20,12 +20,15 @@
1.4 """
1.5
1.6 from datetime import datetime
1.7 -from imiptools.data import get_address, get_uri, get_window_end, \
1.8 - make_freebusy, to_part, \
1.9 +from imiptools.data import Object, get_address, get_uri, get_window_end, \
1.10 + is_new_object, make_freebusy, to_part, \
1.11 uri_dict, uri_items, uri_values
1.12 from imiptools.dates import format_datetime, get_default_timezone, \
1.13 - get_timestamp, to_timezone
1.14 -from imiptools.period import update_freebusy
1.15 + get_recurrence_start_point, get_timestamp, \
1.16 + to_timezone
1.17 +from imiptools.period import can_schedule, remove_period, \
1.18 + remove_additional_periods, remove_affected_period, \
1.19 + update_freebusy
1.20 from imiptools.profile import Preferences
1.21 import imip_store
1.22
1.23 @@ -123,10 +126,30 @@
1.24
1.25 # Common operations on calendar data.
1.26
1.27 - def is_participating(self, attr, as_organiser=False):
1.28 + def is_participating(self, user, as_organiser=False):
1.29 +
1.30 + """
1.31 + Return whether, subject to the 'user' indicating an identity and the
1.32 + 'as_organiser' status of that identity, the user concerned is actually
1.33 + participating in the current object event.
1.34 + """
1.35 +
1.36 + attr = self.get_attendance(user)
1.37 return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED"
1.38
1.39 - def get_overriding_transparency(self, attr, as_organiser=False):
1.40 + def get_overriding_transparency(self, user, as_organiser=False):
1.41 +
1.42 + """
1.43 + Return the overriding transparency to be associated with the free/busy
1.44 + records for an event, subject to the 'user' indicating an identity and
1.45 + the 'as_organiser' status of that identity.
1.46 +
1.47 + Where an identity is only an organiser and not attending, "ORG" is
1.48 + returned. Otherwise, no overriding transparency is defined and None is
1.49 + returned.
1.50 + """
1.51 +
1.52 + attr = self.get_attendance(user)
1.53 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None
1.54
1.55 def update_participation(self, obj, partstat=None):
1.56 @@ -152,12 +175,34 @@
1.57 if self.messenger and self.messenger.sender != get_address(self.user):
1.58 attr["SENT-BY"] = get_uri(self.messenger.sender)
1.59
1.60 + def get_periods(self, obj):
1.61 +
1.62 + """
1.63 + Return periods for the given 'obj'. Interpretation of periods can depend
1.64 + on the time zone, which is obtained for the current user.
1.65 + """
1.66 +
1.67 + return obj.get_periods(self.get_tzid(), self.get_window_end())
1.68 +
1.69 + # Store operations.
1.70 +
1.71 + def get_stored_object(self, uid, recurrenceid):
1.72 +
1.73 + """
1.74 + Return the stored object for the current user, with the given 'uid' and
1.75 + 'recurrenceid'.
1.76 + """
1.77 +
1.78 + fragment = self.store.get_event(self.user, uid, recurrenceid)
1.79 + return fragment and Object(fragment)
1.80 +
1.81 # Free/busy operations.
1.82
1.83 - def get_freebusy_part(self):
1.84 + def get_freebusy_part(self, freebusy=None):
1.85
1.86 """
1.87 - Return a message part containing free/busy information for the user.
1.88 + Return a message part containing free/busy information for the user,
1.89 + either specified as 'freebusy' or obtained from the store directly.
1.90 """
1.91
1.92 if self.is_sharing() and self.is_bundling():
1.93 @@ -167,7 +212,7 @@
1.94 utcnow = get_timestamp()
1.95 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
1.96
1.97 - freebusy = self.store.get_freebusy(self.user)
1.98 + freebusy = freebusy or self.store.get_freebusy(self.user)
1.99
1.100 user_attr = {}
1.101 self.update_sender(user_attr)
1.102 @@ -175,6 +220,17 @@
1.103
1.104 return None
1.105
1.106 + def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser):
1.107 +
1.108 + """
1.109 + Update the 'freebusy' collection with the given 'periods', indicating a
1.110 + 'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a
1.111 + recurrence or the parent event. The 'summary' and 'organiser' must also
1.112 + be provided.
1.113 + """
1.114 +
1.115 + update_freebusy(freebusy, periods, transp, self.uid, recurrenceid, summary, organiser)
1.116 +
1.117 class ClientForObject(Client):
1.118
1.119 "A client maintaining a specific object."
1.120 @@ -184,51 +240,15 @@
1.121 self.set_object(obj)
1.122
1.123 def set_object(self, obj):
1.124 +
1.125 + "Set the current object to 'obj', obtaining metadata details."
1.126 +
1.127 self.obj = obj
1.128 self.uid = obj and self.obj.get_uid()
1.129 self.recurrenceid = obj and self.obj.get_recurrenceid()
1.130 self.sequence = obj and self.obj.get_value("SEQUENCE")
1.131 self.dtstamp = obj and self.obj.get_value("DTSTAMP")
1.132
1.133 - def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None):
1.134 -
1.135 - """
1.136 - Update the 'freebusy' collection with the given 'periods', indicating an
1.137 - explicit 'recurrenceid' to affect either a recurrence or the parent
1.138 - event.
1.139 - """
1.140 -
1.141 - update_freebusy(freebusy, periods,
1.142 - transp or self.obj.get_value("TRANSP") or "OPAQUE",
1.143 - self.uid, recurrenceid,
1.144 - self.obj.get_value("SUMMARY"),
1.145 - self.obj.get_value("ORGANIZER"))
1.146 -
1.147 - def update_freebusy(self, freebusy, periods, transp=None):
1.148 -
1.149 - """
1.150 - Update the 'freebusy' collection for this event with the given
1.151 - 'periods'.
1.152 - """
1.153 -
1.154 - self._update_freebusy(freebusy, periods, self.recurrenceid, transp)
1.155 -
1.156 - def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False):
1.157 -
1.158 - """
1.159 - Update the 'freebusy' collection using the given 'periods', subject to
1.160 - the 'attr' provided for the participant, indicating whether this is
1.161 - being generated 'for_organiser' or not.
1.162 - """
1.163 -
1.164 - # Organisers employ a special transparency if not attending.
1.165 -
1.166 - if self.is_participating(attr, for_organiser):
1.167 - self.update_freebusy(freebusy, periods,
1.168 - transp=self.get_overriding_transparency(attr, for_organiser))
1.169 - else:
1.170 - self.remove_from_freebusy(freebusy)
1.171 -
1.172 # Object update methods.
1.173
1.174 def update_dtstamp(self):
1.175 @@ -246,4 +266,278 @@
1.176 sequence = self.obj.get_value("SEQUENCE") or "0"
1.177 self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]
1.178
1.179 + def merge_attendance(self, attendees):
1.180 +
1.181 + """
1.182 + Merge attendance from the current object's 'attendees' into the version
1.183 + stored for the current user.
1.184 + """
1.185 +
1.186 + obj = self.get_stored_object_version()
1.187 +
1.188 + if not obj or not self.have_new_object(obj):
1.189 + return False
1.190 +
1.191 + # Get attendee details in a usable form.
1.192 +
1.193 + attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
1.194 +
1.195 + for attendee, attendee_attr in attendees.items():
1.196 +
1.197 + # Update attendance in the loaded object.
1.198 +
1.199 + attendee_map[attendee] = attendee_attr
1.200 +
1.201 + # Set the new details and store the object.
1.202 +
1.203 + obj["ATTENDEE"] = attendee_map.items()
1.204 +
1.205 + # Set the complete event if not an additional occurrence.
1.206 +
1.207 + event = obj.to_node()
1.208 + self.store.set_event(self.user, self.uid, self.recurrenceid, event)
1.209 +
1.210 + return True
1.211 +
1.212 + # Object-related tests.
1.213 +
1.214 + def get_attendance(self, user=None):
1.215 +
1.216 + """
1.217 + Return the attendance attributes for 'user', or the current user if
1.218 + 'user' is not specified.
1.219 + """
1.220 +
1.221 + attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
1.222 + return attendees.get(user or self.user) or {}
1.223 +
1.224 + def is_attendee(self, identity, obj=None):
1.225 +
1.226 + """
1.227 + Return whether 'identity' is an attendee in the current object, or in
1.228 + 'obj' if specified.
1.229 + """
1.230 +
1.231 + return identity in uri_values((obj or self.obj).get_values("ATTENDEE"))
1.232 +
1.233 + def can_schedule(self, freebusy, periods):
1.234 +
1.235 + """
1.236 + Indicate whether within 'freebusy' the given 'periods' can be scheduled.
1.237 + """
1.238 +
1.239 + return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
1.240 +
1.241 + def have_new_object(self, obj=None):
1.242 +
1.243 + """
1.244 + Return whether the current object is new to the current user (or if the
1.245 + given 'obj' is new).
1.246 + """
1.247 +
1.248 + obj = obj or self.get_stored_object_version()
1.249 +
1.250 + # If found, compare SEQUENCE and potentially DTSTAMP.
1.251 +
1.252 + if obj:
1.253 + sequence = obj.get_value("SEQUENCE")
1.254 + dtstamp = obj.get_value("DTSTAMP")
1.255 +
1.256 + # If the request refers to an older version of the object, ignore
1.257 + # it.
1.258 +
1.259 + return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp,
1.260 + self.is_partstat_updated(obj))
1.261 +
1.262 + return True
1.263 +
1.264 + def is_partstat_updated(self, obj):
1.265 +
1.266 + """
1.267 + Return whether the participant status has been updated in the current
1.268 + object in comparison to the given 'obj'.
1.269 +
1.270 + NOTE: Some clients like Claws Mail erase time information from DTSTAMP
1.271 + NOTE: and make it invalid. Thus, such attendance information may also be
1.272 + NOTE: incorporated into any new object assessment.
1.273 + """
1.274 +
1.275 + old_attendees = uri_dict(obj.get_value_map("ATTENDEE"))
1.276 + new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
1.277 +
1.278 + for attendee, attr in old_attendees.items():
1.279 + old_partstat = attr.get("PARTSTAT")
1.280 + new_attr = new_attendees.get(attendee)
1.281 + new_partstat = new_attr and new_attr.get("PARTSTAT")
1.282 +
1.283 + if old_partstat == "NEEDS-ACTION" and new_partstat and \
1.284 + new_partstat != old_partstat:
1.285 +
1.286 + return True
1.287 +
1.288 + return False
1.289 +
1.290 + # Object retrieval.
1.291 +
1.292 + def get_stored_object_version(self):
1.293 +
1.294 + """
1.295 + Return the stored object to which the current object refers for the
1.296 + current user.
1.297 + """
1.298 +
1.299 + return self.get_stored_object(self.uid, self.recurrenceid)
1.300 +
1.301 + def get_definitive_object(self, from_organiser):
1.302 +
1.303 + """
1.304 + Return an object considered definitive for the current transaction,
1.305 + using 'from_organiser' to select the current transaction's object if
1.306 + true, or selecting a stored object if false.
1.307 + """
1.308 +
1.309 + return from_organiser and self.obj or self.get_stored_object_version()
1.310 +
1.311 + def get_parent_object(self):
1.312 +
1.313 + """
1.314 + Return the parent object to which the current object refers for the
1.315 + current user.
1.316 + """
1.317 +
1.318 + return self.recurrenceid and self.get_stored_object(self.uid, None) or None
1.319 +
1.320 + # Convenience methods for modifying free/busy collections.
1.321 +
1.322 + def get_recurrence_start_point(self, recurrenceid):
1.323 +
1.324 + "Get 'recurrenceid' in a form suitable for matching free/busy entries."
1.325 +
1.326 + tzid = self.obj.get_tzid() or self.get_tzid()
1.327 + return get_recurrence_start_point(recurrenceid, tzid)
1.328 +
1.329 + def remove_from_freebusy(self, freebusy):
1.330 +
1.331 + "Remove this event from the given 'freebusy' collection."
1.332 +
1.333 + if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid:
1.334 + remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))
1.335 +
1.336 + def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):
1.337 +
1.338 + """
1.339 + Remove from 'freebusy' any original recurrence from parent free/busy
1.340 + details for the current object, if the current object is a specific
1.341 + additional recurrence. Otherwise, remove all additional recurrence
1.342 + information corresponding to 'recurrenceids', or if omitted, all
1.343 + recurrences.
1.344 + """
1.345 +
1.346 + if self.recurrenceid:
1.347 + recurrenceid = self.get_recurrence_start_point(self.recurrenceid)
1.348 + remove_affected_period(freebusy, self.uid, recurrenceid)
1.349 + else:
1.350 + # Remove obsolete recurrence periods.
1.351 +
1.352 + remove_additional_periods(freebusy, self.uid, recurrenceids)
1.353 +
1.354 + # Remove original periods affected by additional recurrences.
1.355 +
1.356 + if recurrenceids:
1.357 + for recurrenceid in recurrenceids:
1.358 + recurrenceid = self.get_recurrence_start_point(recurrenceid)
1.359 + remove_affected_period(freebusy, self.uid, recurrenceid)
1.360 +
1.361 + def update_freebusy(self, freebusy, user, for_organiser):
1.362 +
1.363 + """
1.364 + Update the 'freebusy' collection for this event with the periods and
1.365 + transparency associated with the current object, subject to the 'user'
1.366 + identity and the attendance details provided for them, indicating
1.367 + whether the update is 'for_organiser' or not.
1.368 + """
1.369 +
1.370 + # Obtain the stored object if the current object is not issued by the
1.371 + # organiser. Attendees do not have the opportunity to redefine the
1.372 + # periods.
1.373 +
1.374 + obj = self.get_definitive_object(for_organiser)
1.375 + if not obj:
1.376 + return
1.377 +
1.378 + # Obtain the affected periods.
1.379 +
1.380 + periods = self.get_periods(obj)
1.381 +
1.382 + # Define an overriding transparency, the indicated event transparency,
1.383 + # or the default transparency for the free/busy entry.
1.384 +
1.385 + transp = self.get_overriding_transparency(user, for_organiser) or \
1.386 + obj.get_value("TRANSP") or \
1.387 + "OPAQUE"
1.388 +
1.389 + # Perform the low-level update.
1.390 +
1.391 + Client.update_freebusy(self, freebusy, periods, transp,
1.392 + self.uid, self.recurrenceid,
1.393 + obj.get_value("SUMMARY"),
1.394 + obj.get_value("ORGANIZER"))
1.395 +
1.396 + def update_freebusy_for_participant(self, freebusy, user, for_organiser=False,
1.397 + updating_other=False):
1.398 +
1.399 + """
1.400 + Update the 'freebusy' collection using the given 'periods', involving
1.401 + the given 'user', indicating whether the update is 'for_organiser' or
1.402 + not, and whether it is 'updating_other' (meaning another user's
1.403 + details).
1.404 + """
1.405 +
1.406 + # Record in the free/busy details unless a non-participating attendee.
1.407 + # Use any attendee information for an organiser, not the organiser's own
1.408 + # attributes.
1.409 +
1.410 + if self.is_participating(user, for_organiser and not updating_other):
1.411 + self.update_freebusy(freebusy, user, for_organiser)
1.412 + else:
1.413 + self.remove_from_freebusy(freebusy)
1.414 +
1.415 + # Convenience methods for updating stored free/busy information received
1.416 + # from other users.
1.417 +
1.418 + def update_freebusy_from_participant(self, user, for_organiser):
1.419 +
1.420 + """
1.421 + For the current user, record the free/busy information for another
1.422 + 'user', indicating whether the update is 'for_organiser' or not, thus
1.423 + maintaining a separate record of their free/busy details.
1.424 + """
1.425 +
1.426 + # A user does not store free/busy information for themself as another
1.427 + # party.
1.428 +
1.429 + if user == self.user:
1.430 + return
1.431 +
1.432 + freebusy = self.store.get_freebusy_for_other(self.user, user)
1.433 + self.update_freebusy_for_participant(freebusy, user, for_organiser, True)
1.434 +
1.435 + # Tidy up any obsolete recurrences.
1.436 +
1.437 + self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
1.438 + self.store.set_freebusy_for_other(self.user, freebusy, user)
1.439 +
1.440 + def update_freebusy_from_organiser(self, organiser):
1.441 +
1.442 + "For the current user, record free/busy information from 'organiser'."
1.443 +
1.444 + self.update_freebusy_from_participant(organiser, True)
1.445 +
1.446 + def update_freebusy_from_attendees(self, attendees):
1.447 +
1.448 + "For the current user, record free/busy information from 'attendees'."
1.449 +
1.450 + for attendee in attendees.keys():
1.451 + self.update_freebusy_from_participant(attendee, False)
1.452 +
1.453 # vim: tabstop=4 expandtab shiftwidth=4
2.1 --- a/imiptools/data.py Sun Jul 26 02:01:24 2015 +0200
2.2 +++ b/imiptools/data.py Sun Jul 26 23:43:26 2015 +0200
2.3 @@ -418,12 +418,6 @@
2.4
2.5 return is_same_sequence and partstat_set or not is_old_sequence
2.6
2.7 -# NOTE: Need to expose the 100 day window for recurring events in the
2.8 -# NOTE: configuration.
2.9 -
2.10 -def get_window_end(tzid, window_size=100):
2.11 - return to_timezone(datetime.now(), tzid) + timedelta(window_size)
2.12 -
2.13 def get_periods(obj, tzid, window_end, inclusive=False):
2.14
2.15 """
2.16 @@ -504,4 +498,34 @@
2.17
2.18 return periods
2.19
2.20 +def get_sender_identities(mapping):
2.21 +
2.22 + """
2.23 + Return a mapping from actual senders to the identities for which they
2.24 + have provided data, extracting this information from the given
2.25 + 'mapping'.
2.26 + """
2.27 +
2.28 + senders = {}
2.29 +
2.30 + for value, attr in mapping.items():
2.31 + sent_by = attr.get("SENT-BY")
2.32 + if sent_by:
2.33 + sender = get_uri(sent_by)
2.34 + else:
2.35 + sender = value
2.36 +
2.37 + if not senders.has_key(sender):
2.38 + senders[sender] = []
2.39 +
2.40 + senders[sender].append(value)
2.41 +
2.42 + return senders
2.43 +
2.44 +# NOTE: Need to expose the 100 day window for recurring events in the
2.45 +# NOTE: configuration.
2.46 +
2.47 +def get_window_end(tzid, window_size=100):
2.48 + return to_timezone(datetime.now(), tzid) + timedelta(window_size)
2.49 +
2.50 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/imiptools/handlers/__init__.py Sun Jul 26 02:01:24 2015 +0200
3.2 +++ b/imiptools/handlers/__init__.py Sun Jul 26 23:43:26 2015 +0200
3.3 @@ -22,13 +22,8 @@
3.4 from email.mime.text import MIMEText
3.5 from imiptools.client import ClientForObject
3.6 from imiptools.config import MANAGER_PATH, MANAGER_URL
3.7 -from imiptools.data import Object, get_address, get_uri, \
3.8 - is_new_object, uri_dict, uri_item, uri_values
3.9 -from imiptools.dates import format_datetime, get_recurrence_start_point, \
3.10 - to_timezone
3.11 -from imiptools.period import can_schedule, remove_period, \
3.12 - remove_additional_periods, remove_affected_period
3.13 -from imiptools.profile import Preferences
3.14 +from imiptools.data import get_address, get_uri, get_sender_identities, \
3.15 + uri_dict, uri_item
3.16 from socket import gethostname
3.17
3.18 # References to the Web interface.
3.19 @@ -101,128 +96,8 @@
3.20 def get_outgoing_methods(self):
3.21 return self.outgoing_methods
3.22
3.23 - # Convenience methods for modifying free/busy collections.
3.24 -
3.25 - def get_recurrence_start_point(self, recurrenceid):
3.26 -
3.27 - "Get 'recurrenceid' in a form suitable for matching free/busy entries."
3.28 -
3.29 - tzid = self.obj.get_tzid() or self.get_tzid()
3.30 - return get_recurrence_start_point(recurrenceid, tzid)
3.31 -
3.32 - def remove_from_freebusy(self, freebusy):
3.33 -
3.34 - "Remove this event from the given 'freebusy' collection."
3.35 -
3.36 - if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid:
3.37 - remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))
3.38 -
3.39 - def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):
3.40 -
3.41 - """
3.42 - Remove from 'freebusy' any original recurrence from parent free/busy
3.43 - details for the current object, if the current object is a specific
3.44 - additional recurrence. Otherwise, remove all additional recurrence
3.45 - information corresponding to 'recurrenceids', or if omitted, all
3.46 - recurrences.
3.47 - """
3.48 -
3.49 - if self.recurrenceid:
3.50 - recurrenceid = self.get_recurrence_start_point(self.recurrenceid)
3.51 - remove_affected_period(freebusy, self.uid, recurrenceid)
3.52 - else:
3.53 - # Remove obsolete recurrence periods.
3.54 -
3.55 - remove_additional_periods(freebusy, self.uid, recurrenceids)
3.56 -
3.57 - # Remove original periods affected by additional recurrences.
3.58 -
3.59 - if recurrenceids:
3.60 - for recurrenceid in recurrenceids:
3.61 - recurrenceid = self.get_recurrence_start_point(recurrenceid)
3.62 - remove_affected_period(freebusy, self.uid, recurrenceid)
3.63 -
3.64 - # Convenience methods for updating stored free/busy information.
3.65 -
3.66 - def update_freebusy_from_participant(self, participant_item, for_organiser):
3.67 -
3.68 - """
3.69 - For the calendar user, record the free/busy information for the
3.70 - 'participant_item' (a value plus attributes) representing a different
3.71 - identity, thus maintaining a separate record of their free/busy details.
3.72 - """
3.73 -
3.74 - participant, participant_attr = participant_item
3.75 -
3.76 - # A user does not store free/busy information for themself as another
3.77 - # party.
3.78 -
3.79 - if participant == self.user:
3.80 - return
3.81 -
3.82 - freebusy = self.store.get_freebusy_for_other(self.user, participant)
3.83 -
3.84 - # Obtain the stored object if the current object is not issued by the
3.85 - # organiser.
3.86 -
3.87 - obj = self.get_definitive_object(for_organiser)
3.88 - if not obj:
3.89 - return
3.90 -
3.91 - # Obtain the affected periods.
3.92 -
3.93 - periods = obj.get_periods(self.get_tzid(), self.get_window_end())
3.94 -
3.95 - # Record in the free/busy details unless a non-participating attendee.
3.96 - # Use any attendee information for an organiser, not the organiser's own
3.97 - # attributes.
3.98 -
3.99 - if for_organiser:
3.100 - participant_attr = obj.get_value_map("ATTENDEE").get(participant)
3.101 -
3.102 - self.update_freebusy_for_participant(freebusy, periods, participant_attr,
3.103 - for_organiser and not self.is_attendee(participant))
3.104 -
3.105 - # Tidy up any obsolete recurrences.
3.106 -
3.107 - self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
3.108 - self.store.set_freebusy_for_other(self.user, freebusy, participant)
3.109 -
3.110 - def update_freebusy_from_organiser(self, organiser_item):
3.111 -
3.112 - """
3.113 - For the current user, record free/busy information from the
3.114 - 'organiser_item' (a value plus attributes).
3.115 - """
3.116 -
3.117 - self.update_freebusy_from_participant(organiser_item, True)
3.118 -
3.119 - def update_freebusy_from_attendees(self, attendees):
3.120 -
3.121 - "For the current user, record free/busy information from 'attendees'."
3.122 -
3.123 - for attendee_item in attendees.items():
3.124 - self.update_freebusy_from_participant(attendee_item, False)
3.125 -
3.126 # Logic, filtering and access to calendar structures and other data.
3.127
3.128 - def is_attendee(self, identity, obj=None):
3.129 -
3.130 - """
3.131 - Return whether 'identity' is an attendee in the current object, or in
3.132 - 'obj' if specified.
3.133 - """
3.134 -
3.135 - return identity in uri_values((obj or self.obj).get_values("ATTENDEE"))
3.136 -
3.137 - def can_schedule(self, freebusy, periods):
3.138 -
3.139 - """
3.140 - Indicate whether within 'freebusy' the given 'periods' can be scheduled.
3.141 - """
3.142 -
3.143 - return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
3.144 -
3.145 def filter_by_senders(self, mapping):
3.146
3.147 """
3.148 @@ -233,7 +108,7 @@
3.149
3.150 # Get a mapping from senders to identities.
3.151
3.152 - identities = self.get_sender_identities(mapping)
3.153 + identities = get_sender_identities(mapping)
3.154
3.155 # Find the senders that are valid.
3.156
3.157 @@ -242,7 +117,7 @@
3.158
3.159 # Return the true identities.
3.160
3.161 - return reduce(lambda a, b: a + b, [identities[get_uri(address)] for address in valid])
3.162 + return reduce(lambda a, b: a + b, [identities[get_uri(address)] for address in valid], [])
3.163 else:
3.164 return mapping
3.165
3.166 @@ -317,148 +192,4 @@
3.167
3.168 return organiser_item, attendees
3.169
3.170 - def get_sender_identities(self, mapping):
3.171 -
3.172 - """
3.173 - Return a mapping from actual senders to the identities for which they
3.174 - have provided data, extracting this information from the given
3.175 - 'mapping'.
3.176 - """
3.177 -
3.178 - senders = {}
3.179 -
3.180 - for value, attr in mapping.items():
3.181 - sent_by = attr.get("SENT-BY")
3.182 - if sent_by:
3.183 - sender = get_uri(sent_by)
3.184 - else:
3.185 - sender = value
3.186 -
3.187 - if not senders.has_key(sender):
3.188 - senders[sender] = []
3.189 -
3.190 - senders[sender].append(value)
3.191 -
3.192 - return senders
3.193 -
3.194 - def _get_object(self, uid, recurrenceid):
3.195 -
3.196 - """
3.197 - Return the stored object for the current user, with the given 'uid' and
3.198 - 'recurrenceid'.
3.199 - """
3.200 -
3.201 - fragment = self.store.get_event(self.user, uid, recurrenceid)
3.202 - return fragment and Object(fragment)
3.203 -
3.204 - def get_object(self):
3.205 -
3.206 - """
3.207 - Return the stored object to which the current object refers for the
3.208 - current user.
3.209 - """
3.210 -
3.211 - return self._get_object(self.uid, self.recurrenceid)
3.212 -
3.213 - def get_definitive_object(self, from_organiser):
3.214 -
3.215 - """
3.216 - Return an object considered definitive for the current transaction,
3.217 - using 'from_organiser' to select the current transaction's object if
3.218 - true, or selecting a stored object if false.
3.219 - """
3.220 -
3.221 - return from_organiser and self.obj or self.get_object()
3.222 -
3.223 - def get_parent_object(self):
3.224 -
3.225 - """
3.226 - Return the parent object to which the current object refers for the
3.227 - current user.
3.228 - """
3.229 -
3.230 - return self.recurrenceid and self._get_object(self.uid, None) or None
3.231 -
3.232 - def have_new_object(self, obj=None):
3.233 -
3.234 - """
3.235 - Return whether the current object is new to the current user (or if the
3.236 - given 'obj' is new).
3.237 - """
3.238 -
3.239 - obj = obj or self.get_object()
3.240 -
3.241 - # If found, compare SEQUENCE and potentially DTSTAMP.
3.242 -
3.243 - if obj:
3.244 - sequence = obj.get_value("SEQUENCE")
3.245 - dtstamp = obj.get_value("DTSTAMP")
3.246 -
3.247 - # If the request refers to an older version of the object, ignore
3.248 - # it.
3.249 -
3.250 - return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp,
3.251 - self.is_partstat_updated(obj))
3.252 -
3.253 - return True
3.254 -
3.255 - def is_partstat_updated(self, obj):
3.256 -
3.257 - """
3.258 - Return whether the participant status has been updated in the current
3.259 - object in comparison to the given 'obj'.
3.260 -
3.261 - NOTE: Some clients like Claws Mail erase time information from DTSTAMP
3.262 - NOTE: and make it invalid. Thus, such attendance information may also be
3.263 - NOTE: incorporated into any new object assessment.
3.264 - """
3.265 -
3.266 - old_attendees = uri_dict(obj.get_value_map("ATTENDEE"))
3.267 - new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
3.268 -
3.269 - for attendee, attr in old_attendees.items():
3.270 - old_partstat = attr.get("PARTSTAT")
3.271 - new_attr = new_attendees.get(attendee)
3.272 - new_partstat = new_attr and new_attr.get("PARTSTAT")
3.273 -
3.274 - if old_partstat == "NEEDS-ACTION" and new_partstat and \
3.275 - new_partstat != old_partstat:
3.276 -
3.277 - return True
3.278 -
3.279 - return False
3.280 -
3.281 - def merge_attendance(self, attendees):
3.282 -
3.283 - """
3.284 - Merge attendance from the current object's 'attendees' into the version
3.285 - stored for the current user.
3.286 - """
3.287 -
3.288 - obj = self.get_object()
3.289 -
3.290 - if not obj or not self.have_new_object(obj):
3.291 - return False
3.292 -
3.293 - # Get attendee details in a usable form.
3.294 -
3.295 - attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
3.296 -
3.297 - for attendee, attendee_attr in attendees.items():
3.298 -
3.299 - # Update attendance in the loaded object.
3.300 -
3.301 - attendee_map[attendee] = attendee_attr
3.302 -
3.303 - # Set the new details and store the object.
3.304 -
3.305 - obj["ATTENDEE"] = attendee_map.items()
3.306 -
3.307 - # Set the complete event if not an additional occurrence.
3.308 -
3.309 - event = obj.to_node()
3.310 - self.store.set_event(self.user, self.uid, self.recurrenceid, event)
3.311 -
3.312 - return True
3.313 -
3.314 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/imiptools/handlers/common.py Sun Jul 26 02:01:24 2015 +0200
4.2 +++ b/imiptools/handlers/common.py Sun Jul 26 23:43:26 2015 +0200
4.3 @@ -69,7 +69,7 @@
4.4
4.5 class Outgoing:
4.6
4.7 - "Common outgoing message handling functionality."
4.8 + "Common outgoing message handling functionality mix-in."
4.9
4.10 def update_event_in_freebusy(self, from_organiser=True):
4.11
4.12 @@ -77,25 +77,9 @@
4.13
4.14 freebusy = self.store.get_freebusy(self.user)
4.15
4.16 - # Use the stored event in case the reply is incomplete, as is seen
4.17 - # when Claws sends a REPLY for an object originally employing
4.18 - # recurrence information.
4.19 -
4.20 - obj = self.get_definitive_object(from_organiser)
4.21 - if not obj:
4.22 - return False # although this should not happen
4.23 -
4.24 - # If newer than any old version, discard old details from the
4.25 - # free/busy record and check for suitability.
4.26 -
4.27 - # Interpretation of periods can depend on the time zone.
4.28 -
4.29 - periods = obj.get_periods(self.get_tzid(), self.get_window_end())
4.30 -
4.31 # Obtain the attendance attributes for this user, if available.
4.32
4.33 - attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
4.34 - self.update_freebusy_for_participant(freebusy, periods, attendees.get(self.user), from_organiser)
4.35 + self.update_freebusy_for_participant(freebusy, self.user, from_organiser)
4.36
4.37 # Remove original recurrence details replaced by additional
4.38 # recurrences, as well as obsolete additional recurrences.
5.1 --- a/imiptools/handlers/person.py Sun Jul 26 02:01:24 2015 +0200
5.2 +++ b/imiptools/handlers/person.py Sun Jul 26 23:43:26 2015 +0200
5.3 @@ -42,7 +42,7 @@
5.4 if not oa:
5.5 return False
5.6
5.7 - (organiser, organiser_attr), attendees = organiser_item, attendees = oa
5.8 + (organiser, organiser_attr), attendees = oa
5.9
5.10 # Handle notifications and invitations.
5.11
5.12 @@ -93,7 +93,7 @@
5.13
5.14 # Update the recipient's record of the organiser's schedule.
5.15
5.16 - self.update_freebusy_from_organiser(organiser_item)
5.17 + self.update_freebusy_from_organiser(organiser)
5.18
5.19 # As organiser, update attendance from valid attendees.
5.20
6.1 --- a/imiptools/handlers/person_outgoing.py Sun Jul 26 02:01:24 2015 +0200
6.2 +++ b/imiptools/handlers/person_outgoing.py Sun Jul 26 23:43:26 2015 +0200
6.3 @@ -99,7 +99,7 @@
6.4 # Obtain any stored object, using parent object details if a newly-
6.5 # indicated occurrence is referenced.
6.6
6.7 - obj = self.get_object()
6.8 + obj = self.get_stored_object_version()
6.9 old = not obj and self.get_parent_object() or obj
6.10
6.11 if not old:
7.1 --- a/imipweb/calendar.py Sun Jul 26 02:01:24 2015 +0200
7.2 +++ b/imipweb/calendar.py Sun Jul 26 23:43:26 2015 +0200
7.3 @@ -177,7 +177,7 @@
7.4 page.ul()
7.5
7.6 for uid, recurrenceid in requests:
7.7 - obj = self._get_object(uid, recurrenceid)
7.8 + obj = self.get_stored_object(uid, recurrenceid)
7.9 if obj:
7.10 page.li()
7.11 page.a(obj.get_value("SUMMARY"), href="#request-%s-%s" % (uid, recurrenceid or ""))
8.1 --- a/imipweb/client.py Sun Jul 26 02:01:24 2015 +0200
8.2 +++ b/imipweb/client.py Sun Jul 26 23:43:26 2015 +0200
8.3 @@ -56,22 +56,20 @@
8.4 else:
8.5 recipients = [get_address(organiser)]
8.6
8.7 + # Since the outgoing handler updates this user's free/busy details,
8.8 + # the stored details will probably not have the updated details at
8.9 + # this point, so we update our copy for serialisation as the bundled
8.10 + # free/busy object.
8.11 +
8.12 + freebusy = self.store.get_freebusy(self.user)
8.13 + self.update_freebusy(freebusy, self.user, from_organiser)
8.14 +
8.15 # Bundle free/busy information if appropriate.
8.16
8.17 - part = self.get_freebusy_part()
8.18 + part = self.get_freebusy_part(freebusy)
8.19 if part:
8.20 parts.append(part)
8.21
8.22 - # Since the outgoing handler updates this user's free/busy details,
8.23 - # the stored details will probably not have the updated details at
8.24 - # this point, so we update our copy for serialisation as the bundled
8.25 - # free/busy object.
8.26 -
8.27 - freebusy = self.store.get_freebusy(self.user)
8.28 -
8.29 - self.update_freebusy(freebusy,
8.30 - self.obj.get_periods(self.get_tzid(), self.get_window_end()))
8.31 -
8.32 # Explicitly specify the outgoing BCC recipient since we are sending as
8.33 # the generic calendar user.
8.34
9.1 --- a/imipweb/event.py Sun Jul 26 02:01:24 2015 +0200
9.2 +++ b/imipweb/event.py Sun Jul 26 23:43:26 2015 +0200
9.3 @@ -713,7 +713,7 @@
9.4 recurrenceid = obj.get_recurrenceid()
9.5
9.6 if recurrenceid:
9.7 - parent = self._get_object(uid)
9.8 + parent = self.get_stored_object(uid)
9.9 if not parent:
9.10 return
9.11
9.12 @@ -1101,7 +1101,7 @@
9.13 "Show an object request using the given 'path_info' for the current user."
9.14
9.15 uid, recurrenceid = self._get_identifiers(path_info)
9.16 - obj = self._get_object(uid, recurrenceid)
9.17 + obj = self.get_stored_object(uid, recurrenceid)
9.18
9.19 if not obj:
9.20 return False
10.1 --- a/imipweb/resource.py Sun Jul 26 02:01:24 2015 +0200
10.2 +++ b/imipweb/resource.py Sun Jul 26 23:43:26 2015 +0200
10.3 @@ -110,8 +110,7 @@
10.4 if self.objects.has_key((uid, recurrenceid)):
10.5 return self.objects[(uid, recurrenceid)]
10.6
10.7 - fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None
10.8 - obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment)
10.9 + obj = self.objects[(uid, recurrenceid)] = self.get_stored_object(uid, recurrenceid)
10.10 return obj
10.11
10.12 def _get_recurrences(self, uid):
10.13 @@ -127,7 +126,7 @@
10.14 def _get_request_summary(self):
10.15 summary = []
10.16 for uid, recurrenceid in self._get_requests():
10.17 - obj = self._get_object(uid, recurrenceid)
10.18 + obj = self.get_stored_object(uid, recurrenceid)
10.19 if obj:
10.20 periods = obj.get_periods(self.get_tzid(), self.get_window_end())
10.21 recurrenceids = self._get_recurrences(uid)
10.22 @@ -203,8 +202,7 @@
10.23
10.24 freebusy = self.store.get_freebusy(self.user)
10.25
10.26 - update_freebusy(freebusy,
10.27 - obj.get_periods(self.get_tzid(), self.get_window_end()),
10.28 + Client.update_freebusy(self, freebusy, self.get_periods(obj),
10.29 is_only_organiser and "ORG" or obj.get_value("TRANSP"),
10.30 uid, recurrenceid,
10.31 obj.get_value("SUMMARY"),