1.1 --- a/imip_manager.py Sun Mar 22 18:36:34 2015 +0100
1.2 +++ b/imip_manager.py Sun Mar 22 18:37:13 2015 +0100
1.3 @@ -31,7 +31,6 @@
1.4
1.5 sys.path.append(LIBRARY_PATH)
1.6
1.7 -from imiptools.content import Handler
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 @@ -39,6 +38,7 @@
1.12 get_datetime_item, get_default_timezone, \
1.13 get_end_of_day, get_start_of_day, get_start_of_next_day, \
1.14 get_timestamp, ends_on_same_day, to_timezone
1.15 +from imiptools.handlers import Handler
1.16 from imiptools.mail import Messenger
1.17 from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
1.18 convert_periods, get_freebusy_details, \
2.1 --- a/imiptools/content.py Sun Mar 22 18:36:34 2015 +0100
2.2 +++ b/imiptools/content.py Sun Mar 22 18:37:13 2015 +0100
2.3 @@ -20,19 +20,7 @@
2.4 this program. If not, see <http://www.gnu.org/licenses/>.
2.5 """
2.6
2.7 -from datetime import datetime, timedelta
2.8 -from email.mime.text import MIMEText
2.9 -from imiptools.config import MANAGER_PATH, MANAGER_URL
2.10 -from imiptools.data import Object, parse_object, \
2.11 - get_address, get_uri, get_value, get_window_end, \
2.12 - is_new_object, uri_dict, uri_item, uri_values
2.13 -from imiptools.dates import format_datetime, get_default_timezone, to_timezone
2.14 -from imiptools.period import can_schedule, insert_period, remove_period, \
2.15 - remove_additional_periods, remove_affected_period, \
2.16 - update_freebusy
2.17 -from imiptools.profile import Preferences
2.18 -from socket import gethostname
2.19 -import imip_store
2.20 +from imiptools.data import Object, parse_object, get_value
2.21
2.22 try:
2.23 from cStringIO import StringIO
2.24 @@ -86,458 +74,6 @@
2.25 handler.set_object(Object({name : item}))
2.26 methods[method](handler)()
2.27
2.28 -# References to the Web interface.
2.29 -
2.30 -def get_manager_url():
2.31 - url_base = MANAGER_URL or "http://%s/" % gethostname()
2.32 - return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/"))
2.33 -
2.34 -def get_object_url(uid, recurrenceid=None):
2.35 - return "%s/%s%s" % (
2.36 - get_manager_url().rstrip("/"), uid,
2.37 - recurrenceid and "/%s" % recurrenceid or ""
2.38 - )
2.39 -
2.40 -class Handler:
2.41 -
2.42 - "General handler support."
2.43 -
2.44 - def __init__(self, senders=None, recipient=None, messenger=None):
2.45 -
2.46 - """
2.47 - Initialise the handler with the calendar 'obj' and the 'senders' and
2.48 - 'recipient' of the object (if specifically indicated).
2.49 - """
2.50 -
2.51 - self.senders = senders and set(map(get_address, senders))
2.52 - self.recipient = recipient and get_address(recipient)
2.53 - self.messenger = messenger
2.54 -
2.55 - self.results = []
2.56 - self.outgoing_methods = set()
2.57 -
2.58 - self.obj = None
2.59 - self.uid = None
2.60 - self.recurrenceid = None
2.61 - self.sequence = None
2.62 - self.dtstamp = None
2.63 -
2.64 - self.store = imip_store.FileStore()
2.65 -
2.66 - try:
2.67 - self.publisher = imip_store.FilePublisher()
2.68 - except OSError:
2.69 - self.publisher = None
2.70 -
2.71 - def set_object(self, obj):
2.72 - self.obj = obj
2.73 - self.uid = self.obj.get_value("UID")
2.74 - self.recurrenceid = format_datetime(self.obj.get_utc_datetime("RECURRENCE-ID"))
2.75 - self.sequence = self.obj.get_value("SEQUENCE")
2.76 - self.dtstamp = self.obj.get_value("DTSTAMP")
2.77 -
2.78 - def wrap(self, text, link=True):
2.79 -
2.80 - "Wrap any valid message for passing to the recipient."
2.81 -
2.82 - texts = []
2.83 - texts.append(text)
2.84 - if link:
2.85 - texts.append("If your mail program cannot handle this "
2.86 - "message, you may view the details here:\n\n%s" %
2.87 - get_object_url(self.uid, self.recurrenceid))
2.88 -
2.89 - return self.add_result(None, None, MIMEText("\n".join(texts)))
2.90 -
2.91 - # Result registration.
2.92 -
2.93 - def add_result(self, method, outgoing_recipients, part):
2.94 -
2.95 - """
2.96 - Record a result having the given 'method', 'outgoing_recipients' and
2.97 - message part.
2.98 - """
2.99 -
2.100 - if outgoing_recipients:
2.101 - self.outgoing_methods.add(method)
2.102 - self.results.append((outgoing_recipients, part))
2.103 -
2.104 - def get_results(self):
2.105 - return self.results
2.106 -
2.107 - def get_outgoing_methods(self):
2.108 - return self.outgoing_methods
2.109 -
2.110 - # Convenience methods for modifying free/busy collections.
2.111 -
2.112 - def remove_from_freebusy(self, freebusy):
2.113 -
2.114 - "Remove this event from the given 'freebusy' collection."
2.115 -
2.116 - remove_period(freebusy, self.uid, self.recurrenceid)
2.117 -
2.118 - def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):
2.119 -
2.120 - """
2.121 - Remove from 'freebusy' any original recurrence from parent free/busy
2.122 - details for the current object, if the current object is a specific
2.123 - additional recurrence. Otherwise, remove all additional recurrence
2.124 - information corresponding to 'recurrenceids', or if omitted, all
2.125 - recurrences.
2.126 - """
2.127 -
2.128 - if self.recurrenceid:
2.129 - remove_affected_period(freebusy, self.uid, self.recurrenceid)
2.130 - else:
2.131 - # Remove obsolete recurrence periods.
2.132 -
2.133 - remove_additional_periods(freebusy, self.uid, recurrenceids)
2.134 -
2.135 - # Remove original periods affected by additional recurrences.
2.136 -
2.137 - if recurrenceids:
2.138 - for recurrenceid in recurrenceids:
2.139 - remove_affected_period(freebusy, self.uid, recurrenceid)
2.140 -
2.141 - def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None):
2.142 -
2.143 - """
2.144 - Update the 'freebusy' collection with the given 'periods', indicating an
2.145 - explicit 'recurrenceid' to affect either a recurrence or the parent
2.146 - event.
2.147 - """
2.148 -
2.149 - update_freebusy(freebusy, periods,
2.150 - transp or self.obj.get_value("TRANSP"),
2.151 - self.uid, recurrenceid,
2.152 - self.obj.get_value("SUMMARY"),
2.153 - self.obj.get_value("ORGANIZER"))
2.154 -
2.155 - def update_freebusy(self, freebusy, periods, transp=None):
2.156 -
2.157 - """
2.158 - Update the 'freebusy' collection for this event with the given
2.159 - 'periods'.
2.160 - """
2.161 -
2.162 - self._update_freebusy(freebusy, periods, self.recurrenceid, transp)
2.163 -
2.164 - def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False):
2.165 -
2.166 - """
2.167 - Update the 'freebusy' collection using the given 'periods', subject to
2.168 - the 'attr' provided for the participant, indicating whether this is
2.169 - being generated 'for_organiser' or not.
2.170 - """
2.171 -
2.172 - # Organisers employ a special transparency.
2.173 -
2.174 - if for_organiser or attr.get("PARTSTAT") != "DECLINED":
2.175 - self.update_freebusy(freebusy, periods, transp=(for_organiser and "ORG" or None))
2.176 - else:
2.177 - self.remove_from_freebusy(freebusy)
2.178 -
2.179 - # Convenience methods for updating stored free/busy information.
2.180 -
2.181 - def update_freebusy_from_participant(self, user, participant_item, for_organiser):
2.182 -
2.183 - """
2.184 - For the given 'user', record the free/busy information for the
2.185 - 'participant_item' (a value plus attributes) representing a different
2.186 - identity, thus maintaining a separate record of their free/busy details.
2.187 - """
2.188 -
2.189 - participant, participant_attr = participant_item
2.190 -
2.191 - if participant == user:
2.192 - return
2.193 -
2.194 - freebusy = self.store.get_freebusy_for_other(user, participant)
2.195 - tzid = self.get_tzid(user)
2.196 - window_end = get_window_end(tzid)
2.197 - periods = self.obj.get_periods_for_freebusy(tzid, window_end)
2.198 -
2.199 - # Record in the free/busy details unless a non-participating attendee.
2.200 -
2.201 - self.update_freebusy_for_participant(freebusy, periods, participant_attr,
2.202 - for_organiser and self.is_not_attendee(participant, self.obj))
2.203 -
2.204 - self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(user, self.uid))
2.205 - self.store.set_freebusy_for_other(user, freebusy, participant)
2.206 -
2.207 - def update_freebusy_from_organiser(self, attendee, organiser_item):
2.208 -
2.209 - """
2.210 - For the 'attendee', record free/busy information from the
2.211 - 'organiser_item' (a value plus attributes).
2.212 - """
2.213 -
2.214 - self.update_freebusy_from_participant(attendee, organiser_item, True)
2.215 -
2.216 - def update_freebusy_from_attendees(self, organiser, attendees):
2.217 -
2.218 - "For the 'organiser', record free/busy information from 'attendees'."
2.219 -
2.220 - for attendee_item in attendees.items():
2.221 - self.update_freebusy_from_participant(organiser, attendee_item, False)
2.222 -
2.223 - # Logic, filtering and access to calendar structures and other data.
2.224 -
2.225 - def is_not_attendee(self, identity, obj):
2.226 -
2.227 - "Return whether 'identity' is not an attendee in 'obj'."
2.228 -
2.229 - return identity not in uri_values(obj.get_values("ATTENDEE"))
2.230 -
2.231 - def can_schedule(self, freebusy, periods):
2.232 - return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
2.233 -
2.234 - def filter_by_senders(self, mapping):
2.235 -
2.236 - """
2.237 - Return a list of items from 'mapping' filtered using sender information.
2.238 - """
2.239 -
2.240 - if self.senders:
2.241 -
2.242 - # Get a mapping from senders to identities.
2.243 -
2.244 - identities = self.get_sender_identities(mapping)
2.245 -
2.246 - # Find the senders that are valid.
2.247 -
2.248 - senders = map(get_address, identities)
2.249 - valid = self.senders.intersection(senders)
2.250 -
2.251 - # Return the true identities.
2.252 -
2.253 - return [identities[get_uri(address)] for address in valid]
2.254 - else:
2.255 - return mapping
2.256 -
2.257 - def filter_by_recipient(self, mapping):
2.258 -
2.259 - """
2.260 - Return a list of items from 'mapping' filtered using recipient
2.261 - information.
2.262 - """
2.263 -
2.264 - if self.recipient:
2.265 - addresses = set(map(get_address, mapping))
2.266 - return map(get_uri, addresses.intersection([self.recipient]))
2.267 - else:
2.268 - return mapping
2.269 -
2.270 - def require_organiser(self, from_organiser=True):
2.271 -
2.272 - """
2.273 - Return the organiser for the current object, filtered for the sender or
2.274 - recipient of interest. Return None if no identities are eligible.
2.275 -
2.276 - The organiser identity is normalized.
2.277 - """
2.278 -
2.279 - organiser_item = uri_item(self.obj.get_item("ORGANIZER"))
2.280 -
2.281 - # Only provide details for an organiser who sent/receives the message.
2.282 -
2.283 - organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient
2.284 -
2.285 - if not organiser_filter_fn(dict([organiser_item])):
2.286 - return None
2.287 -
2.288 - return organiser_item
2.289 -
2.290 - def require_attendees(self, from_organiser=True):
2.291 -
2.292 - """
2.293 - Return the attendees for the current object, filtered for the sender or
2.294 - recipient of interest. Return None if no identities are eligible.
2.295 -
2.296 - The attendee identities are normalized.
2.297 - """
2.298 -
2.299 - attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
2.300 -
2.301 - # Only provide details for attendees who sent/receive the message.
2.302 -
2.303 - attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders
2.304 -
2.305 - attendees = {}
2.306 - for attendee in attendee_filter_fn(attendee_map):
2.307 - attendees[attendee] = attendee_map[attendee]
2.308 -
2.309 - return attendees
2.310 -
2.311 - def require_organiser_and_attendees(self, from_organiser=True):
2.312 -
2.313 - """
2.314 - Return the organiser and attendees for the current object, filtered for
2.315 - the recipient of interest. Return None if no identities are eligible.
2.316 -
2.317 - Organiser and attendee identities are normalized.
2.318 - """
2.319 -
2.320 - organiser_item = self.require_organiser(from_organiser)
2.321 - attendees = self.require_attendees(from_organiser)
2.322 -
2.323 - if not attendees or not organiser_item:
2.324 - return None
2.325 -
2.326 - return organiser_item, attendees
2.327 -
2.328 - def get_sender_identities(self, mapping):
2.329 -
2.330 - """
2.331 - Return a mapping from actual senders to the identities for which they
2.332 - have provided data, extracting this information from the given
2.333 - 'mapping'.
2.334 - """
2.335 -
2.336 - senders = {}
2.337 -
2.338 - for value, attr in mapping.items():
2.339 - sent_by = attr.get("SENT-BY")
2.340 - if sent_by:
2.341 - senders[get_uri(sent_by)] = value
2.342 - else:
2.343 - senders[value] = value
2.344 -
2.345 - return senders
2.346 -
2.347 - def _get_object(self, user, uid, recurrenceid):
2.348 -
2.349 - """
2.350 - Return the stored object for the given 'user', 'uid' and 'recurrenceid'.
2.351 - """
2.352 -
2.353 - fragment = self.store.get_event(user, uid, recurrenceid)
2.354 - return fragment and Object(fragment)
2.355 -
2.356 - def get_object(self, user):
2.357 -
2.358 - """
2.359 - Return the stored object to which the current object refers for the
2.360 - given 'user'.
2.361 - """
2.362 -
2.363 - return self._get_object(user, self.uid, self.recurrenceid)
2.364 -
2.365 - def get_parent_object(self, user):
2.366 -
2.367 - """
2.368 - Return the parent object to which the current object refers for the
2.369 - given 'user'.
2.370 - """
2.371 -
2.372 - return self.recurrenceid and self._get_object(user, self.uid, None) or None
2.373 -
2.374 - def have_new_object(self, attendee, obj=None):
2.375 -
2.376 - """
2.377 - Return whether the current object is new to the 'attendee' (or if the
2.378 - given 'obj' is new).
2.379 - """
2.380 -
2.381 - obj = obj or self.get_object(attendee)
2.382 -
2.383 - # If found, compare SEQUENCE and potentially DTSTAMP.
2.384 -
2.385 - if obj:
2.386 - sequence = obj.get_value("SEQUENCE")
2.387 - dtstamp = obj.get_value("DTSTAMP")
2.388 -
2.389 - # If the request refers to an older version of the object, ignore
2.390 - # it.
2.391 -
2.392 - return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp,
2.393 - self.is_partstat_updated(obj))
2.394 -
2.395 - return True
2.396 -
2.397 - def is_partstat_updated(self, obj):
2.398 -
2.399 - """
2.400 - Return whether the participant status has been updated in the current
2.401 - object in comparison to the given 'obj'.
2.402 -
2.403 - NOTE: Some clients like Claws Mail erase time information from DTSTAMP
2.404 - NOTE: and make it invalid. Thus, such attendance information may also be
2.405 - NOTE: incorporated into any new object assessment.
2.406 - """
2.407 -
2.408 - old_attendees = uri_dict(obj.get_value_map("ATTENDEE"))
2.409 - new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
2.410 -
2.411 - for attendee, attr in old_attendees.items():
2.412 - old_partstat = attr.get("PARTSTAT")
2.413 - new_attr = new_attendees.get(attendee)
2.414 - new_partstat = new_attr and new_attr.get("PARTSTAT")
2.415 -
2.416 - if old_partstat == "NEEDS-ACTION" and new_partstat and \
2.417 - new_partstat != old_partstat:
2.418 -
2.419 - return True
2.420 -
2.421 - return False
2.422 -
2.423 - def merge_attendance(self, attendees, identity):
2.424 -
2.425 - """
2.426 - Merge attendance from the current object's 'attendees' into the version
2.427 - stored for the given 'identity'.
2.428 - """
2.429 -
2.430 - obj = self.get_object(identity)
2.431 -
2.432 - if not obj or not self.have_new_object(identity, obj=obj):
2.433 - return False
2.434 -
2.435 - # Get attendee details in a usable form.
2.436 -
2.437 - attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
2.438 -
2.439 - for attendee, attendee_attr in attendees.items():
2.440 -
2.441 - # Update attendance in the loaded object.
2.442 -
2.443 - attendee_map[attendee] = attendee_attr
2.444 -
2.445 - # Set the new details and store the object.
2.446 -
2.447 - obj["ATTENDEE"] = attendee_map.items()
2.448 -
2.449 - # Set the complete event if not an additional occurrence.
2.450 -
2.451 - event = obj.to_node()
2.452 - recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
2.453 -
2.454 - self.store.set_event(identity, self.uid, self.recurrenceid, event)
2.455 -
2.456 - return True
2.457 -
2.458 - def update_dtstamp(self):
2.459 -
2.460 - "Update the DTSTAMP in the current object."
2.461 -
2.462 - dtstamp = self.obj.get_utc_datetime("DTSTAMP")
2.463 - utcnow = to_timezone(datetime.utcnow(), "UTC")
2.464 - self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})]
2.465 -
2.466 - def set_sequence(self, increment=False):
2.467 -
2.468 - "Update the SEQUENCE in the current object."
2.469 -
2.470 - sequence = self.obj.get_value("SEQUENCE") or "0"
2.471 - self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]
2.472 -
2.473 - def get_tzid(self, identity):
2.474 -
2.475 - "Return the time regime applicable for the given 'identity'."
2.476 -
2.477 - preferences = Preferences(identity)
2.478 - return preferences.get("TZID") or get_default_timezone()
2.479 -
2.480 # Handler registry.
2.481
2.482 methods = {
3.1 --- a/imiptools/handlers/__init__.py Sun Mar 22 18:36:34 2015 +0100
3.2 +++ b/imiptools/handlers/__init__.py Sun Mar 22 18:37:13 2015 +0100
3.3 @@ -0,0 +1,488 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +General handler support for incoming calendar objects.
3.8 +
3.9 +Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
3.10 +
3.11 +This program is free software; you can redistribute it and/or modify it under
3.12 +the terms of the GNU General Public License as published by the Free Software
3.13 +Foundation; either version 3 of the License, or (at your option) any later
3.14 +version.
3.15 +
3.16 +This program is distributed in the hope that it will be useful, but WITHOUT
3.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3.19 +details.
3.20 +
3.21 +You should have received a copy of the GNU General Public License along with
3.22 +this program. If not, see <http://www.gnu.org/licenses/>.
3.23 +"""
3.24 +
3.25 +from datetime import datetime
3.26 +from email.mime.text import MIMEText
3.27 +from imiptools.config import MANAGER_PATH, MANAGER_URL
3.28 +from imiptools.data import Object, \
3.29 + get_address, get_uri, get_value, get_window_end, \
3.30 + is_new_object, uri_dict, uri_item, uri_values
3.31 +from imiptools.dates import format_datetime, get_default_timezone, to_timezone
3.32 +from imiptools.period import can_schedule, remove_period, \
3.33 + remove_additional_periods, remove_affected_period, \
3.34 + update_freebusy
3.35 +from imiptools.profile import Preferences
3.36 +from socket import gethostname
3.37 +import imip_store
3.38 +
3.39 +# References to the Web interface.
3.40 +
3.41 +def get_manager_url():
3.42 + url_base = MANAGER_URL or "http://%s/" % gethostname()
3.43 + return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/"))
3.44 +
3.45 +def get_object_url(uid, recurrenceid=None):
3.46 + return "%s/%s%s" % (
3.47 + get_manager_url().rstrip("/"), uid,
3.48 + recurrenceid and "/%s" % recurrenceid or ""
3.49 + )
3.50 +
3.51 +class Handler:
3.52 +
3.53 + "General handler support."
3.54 +
3.55 + def __init__(self, senders=None, recipient=None, messenger=None):
3.56 +
3.57 + """
3.58 + Initialise the handler with the calendar 'obj' and the 'senders' and
3.59 + 'recipient' of the object (if specifically indicated).
3.60 + """
3.61 +
3.62 + self.senders = senders and set(map(get_address, senders))
3.63 + self.recipient = recipient and get_address(recipient)
3.64 + self.messenger = messenger
3.65 +
3.66 + self.results = []
3.67 + self.outgoing_methods = set()
3.68 +
3.69 + self.obj = None
3.70 + self.uid = None
3.71 + self.recurrenceid = None
3.72 + self.sequence = None
3.73 + self.dtstamp = None
3.74 +
3.75 + self.store = imip_store.FileStore()
3.76 +
3.77 + try:
3.78 + self.publisher = imip_store.FilePublisher()
3.79 + except OSError:
3.80 + self.publisher = None
3.81 +
3.82 + def set_object(self, obj):
3.83 + self.obj = obj
3.84 + self.uid = self.obj.get_value("UID")
3.85 + self.recurrenceid = format_datetime(self.obj.get_utc_datetime("RECURRENCE-ID"))
3.86 + self.sequence = self.obj.get_value("SEQUENCE")
3.87 + self.dtstamp = self.obj.get_value("DTSTAMP")
3.88 +
3.89 + def wrap(self, text, link=True):
3.90 +
3.91 + "Wrap any valid message for passing to the recipient."
3.92 +
3.93 + texts = []
3.94 + texts.append(text)
3.95 + if link:
3.96 + texts.append("If your mail program cannot handle this "
3.97 + "message, you may view the details here:\n\n%s" %
3.98 + get_object_url(self.uid, self.recurrenceid))
3.99 +
3.100 + return self.add_result(None, None, MIMEText("\n".join(texts)))
3.101 +
3.102 + # Result registration.
3.103 +
3.104 + def add_result(self, method, outgoing_recipients, part):
3.105 +
3.106 + """
3.107 + Record a result having the given 'method', 'outgoing_recipients' and
3.108 + message part.
3.109 + """
3.110 +
3.111 + if outgoing_recipients:
3.112 + self.outgoing_methods.add(method)
3.113 + self.results.append((outgoing_recipients, part))
3.114 +
3.115 + def get_results(self):
3.116 + return self.results
3.117 +
3.118 + def get_outgoing_methods(self):
3.119 + return self.outgoing_methods
3.120 +
3.121 + # Convenience methods for modifying free/busy collections.
3.122 +
3.123 + def remove_from_freebusy(self, freebusy):
3.124 +
3.125 + "Remove this event from the given 'freebusy' collection."
3.126 +
3.127 + remove_period(freebusy, self.uid, self.recurrenceid)
3.128 +
3.129 + def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):
3.130 +
3.131 + """
3.132 + Remove from 'freebusy' any original recurrence from parent free/busy
3.133 + details for the current object, if the current object is a specific
3.134 + additional recurrence. Otherwise, remove all additional recurrence
3.135 + information corresponding to 'recurrenceids', or if omitted, all
3.136 + recurrences.
3.137 + """
3.138 +
3.139 + if self.recurrenceid:
3.140 + remove_affected_period(freebusy, self.uid, self.recurrenceid)
3.141 + else:
3.142 + # Remove obsolete recurrence periods.
3.143 +
3.144 + remove_additional_periods(freebusy, self.uid, recurrenceids)
3.145 +
3.146 + # Remove original periods affected by additional recurrences.
3.147 +
3.148 + if recurrenceids:
3.149 + for recurrenceid in recurrenceids:
3.150 + remove_affected_period(freebusy, self.uid, recurrenceid)
3.151 +
3.152 + def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None):
3.153 +
3.154 + """
3.155 + Update the 'freebusy' collection with the given 'periods', indicating an
3.156 + explicit 'recurrenceid' to affect either a recurrence or the parent
3.157 + event.
3.158 + """
3.159 +
3.160 + update_freebusy(freebusy, periods,
3.161 + transp or self.obj.get_value("TRANSP"),
3.162 + self.uid, recurrenceid,
3.163 + self.obj.get_value("SUMMARY"),
3.164 + self.obj.get_value("ORGANIZER"))
3.165 +
3.166 + def update_freebusy(self, freebusy, periods, transp=None):
3.167 +
3.168 + """
3.169 + Update the 'freebusy' collection for this event with the given
3.170 + 'periods'.
3.171 + """
3.172 +
3.173 + self._update_freebusy(freebusy, periods, self.recurrenceid, transp)
3.174 +
3.175 + def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False):
3.176 +
3.177 + """
3.178 + Update the 'freebusy' collection using the given 'periods', subject to
3.179 + the 'attr' provided for the participant, indicating whether this is
3.180 + being generated 'for_organiser' or not.
3.181 + """
3.182 +
3.183 + # Organisers employ a special transparency.
3.184 +
3.185 + if for_organiser or attr.get("PARTSTAT") != "DECLINED":
3.186 + self.update_freebusy(freebusy, periods, transp=(for_organiser and "ORG" or None))
3.187 + else:
3.188 + self.remove_from_freebusy(freebusy)
3.189 +
3.190 + # Convenience methods for updating stored free/busy information.
3.191 +
3.192 + def update_freebusy_from_participant(self, user, participant_item, for_organiser):
3.193 +
3.194 + """
3.195 + For the given 'user', record the free/busy information for the
3.196 + 'participant_item' (a value plus attributes) representing a different
3.197 + identity, thus maintaining a separate record of their free/busy details.
3.198 + """
3.199 +
3.200 + participant, participant_attr = participant_item
3.201 +
3.202 + if participant == user:
3.203 + return
3.204 +
3.205 + freebusy = self.store.get_freebusy_for_other(user, participant)
3.206 + tzid = self.get_tzid(user)
3.207 + window_end = get_window_end(tzid)
3.208 + periods = self.obj.get_periods_for_freebusy(tzid, window_end)
3.209 +
3.210 + # Record in the free/busy details unless a non-participating attendee.
3.211 +
3.212 + self.update_freebusy_for_participant(freebusy, periods, participant_attr,
3.213 + for_organiser and self.is_not_attendee(participant, self.obj))
3.214 +
3.215 + self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(user, self.uid))
3.216 + self.store.set_freebusy_for_other(user, freebusy, participant)
3.217 +
3.218 + def update_freebusy_from_organiser(self, attendee, organiser_item):
3.219 +
3.220 + """
3.221 + For the 'attendee', record free/busy information from the
3.222 + 'organiser_item' (a value plus attributes).
3.223 + """
3.224 +
3.225 + self.update_freebusy_from_participant(attendee, organiser_item, True)
3.226 +
3.227 + def update_freebusy_from_attendees(self, organiser, attendees):
3.228 +
3.229 + "For the 'organiser', record free/busy information from 'attendees'."
3.230 +
3.231 + for attendee_item in attendees.items():
3.232 + self.update_freebusy_from_participant(organiser, attendee_item, False)
3.233 +
3.234 + # Logic, filtering and access to calendar structures and other data.
3.235 +
3.236 + def is_not_attendee(self, identity, obj):
3.237 +
3.238 + "Return whether 'identity' is not an attendee in 'obj'."
3.239 +
3.240 + return identity not in uri_values(obj.get_values("ATTENDEE"))
3.241 +
3.242 + def can_schedule(self, freebusy, periods):
3.243 + return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
3.244 +
3.245 + def filter_by_senders(self, mapping):
3.246 +
3.247 + """
3.248 + Return a list of items from 'mapping' filtered using sender information.
3.249 + """
3.250 +
3.251 + if self.senders:
3.252 +
3.253 + # Get a mapping from senders to identities.
3.254 +
3.255 + identities = self.get_sender_identities(mapping)
3.256 +
3.257 + # Find the senders that are valid.
3.258 +
3.259 + senders = map(get_address, identities)
3.260 + valid = self.senders.intersection(senders)
3.261 +
3.262 + # Return the true identities.
3.263 +
3.264 + return [identities[get_uri(address)] for address in valid]
3.265 + else:
3.266 + return mapping
3.267 +
3.268 + def filter_by_recipient(self, mapping):
3.269 +
3.270 + """
3.271 + Return a list of items from 'mapping' filtered using recipient
3.272 + information.
3.273 + """
3.274 +
3.275 + if self.recipient:
3.276 + addresses = set(map(get_address, mapping))
3.277 + return map(get_uri, addresses.intersection([self.recipient]))
3.278 + else:
3.279 + return mapping
3.280 +
3.281 + def require_organiser(self, from_organiser=True):
3.282 +
3.283 + """
3.284 + Return the organiser for the current object, filtered for the sender or
3.285 + recipient of interest. Return None if no identities are eligible.
3.286 +
3.287 + The organiser identity is normalized.
3.288 + """
3.289 +
3.290 + organiser_item = uri_item(self.obj.get_item("ORGANIZER"))
3.291 +
3.292 + # Only provide details for an organiser who sent/receives the message.
3.293 +
3.294 + organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient
3.295 +
3.296 + if not organiser_filter_fn(dict([organiser_item])):
3.297 + return None
3.298 +
3.299 + return organiser_item
3.300 +
3.301 + def require_attendees(self, from_organiser=True):
3.302 +
3.303 + """
3.304 + Return the attendees for the current object, filtered for the sender or
3.305 + recipient of interest. Return None if no identities are eligible.
3.306 +
3.307 + The attendee identities are normalized.
3.308 + """
3.309 +
3.310 + attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
3.311 +
3.312 + # Only provide details for attendees who sent/receive the message.
3.313 +
3.314 + attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders
3.315 +
3.316 + attendees = {}
3.317 + for attendee in attendee_filter_fn(attendee_map):
3.318 + attendees[attendee] = attendee_map[attendee]
3.319 +
3.320 + return attendees
3.321 +
3.322 + def require_organiser_and_attendees(self, from_organiser=True):
3.323 +
3.324 + """
3.325 + Return the organiser and attendees for the current object, filtered for
3.326 + the recipient of interest. Return None if no identities are eligible.
3.327 +
3.328 + Organiser and attendee identities are normalized.
3.329 + """
3.330 +
3.331 + organiser_item = self.require_organiser(from_organiser)
3.332 + attendees = self.require_attendees(from_organiser)
3.333 +
3.334 + if not attendees or not organiser_item:
3.335 + return None
3.336 +
3.337 + return organiser_item, attendees
3.338 +
3.339 + def get_sender_identities(self, mapping):
3.340 +
3.341 + """
3.342 + Return a mapping from actual senders to the identities for which they
3.343 + have provided data, extracting this information from the given
3.344 + 'mapping'.
3.345 + """
3.346 +
3.347 + senders = {}
3.348 +
3.349 + for value, attr in mapping.items():
3.350 + sent_by = attr.get("SENT-BY")
3.351 + if sent_by:
3.352 + senders[get_uri(sent_by)] = value
3.353 + else:
3.354 + senders[value] = value
3.355 +
3.356 + return senders
3.357 +
3.358 + def _get_object(self, user, uid, recurrenceid):
3.359 +
3.360 + """
3.361 + Return the stored object for the given 'user', 'uid' and 'recurrenceid'.
3.362 + """
3.363 +
3.364 + fragment = self.store.get_event(user, uid, recurrenceid)
3.365 + return fragment and Object(fragment)
3.366 +
3.367 + def get_object(self, user):
3.368 +
3.369 + """
3.370 + Return the stored object to which the current object refers for the
3.371 + given 'user'.
3.372 + """
3.373 +
3.374 + return self._get_object(user, self.uid, self.recurrenceid)
3.375 +
3.376 + def get_parent_object(self, user):
3.377 +
3.378 + """
3.379 + Return the parent object to which the current object refers for the
3.380 + given 'user'.
3.381 + """
3.382 +
3.383 + return self.recurrenceid and self._get_object(user, self.uid, None) or None
3.384 +
3.385 + def have_new_object(self, attendee, obj=None):
3.386 +
3.387 + """
3.388 + Return whether the current object is new to the 'attendee' (or if the
3.389 + given 'obj' is new).
3.390 + """
3.391 +
3.392 + obj = obj or self.get_object(attendee)
3.393 +
3.394 + # If found, compare SEQUENCE and potentially DTSTAMP.
3.395 +
3.396 + if obj:
3.397 + sequence = obj.get_value("SEQUENCE")
3.398 + dtstamp = obj.get_value("DTSTAMP")
3.399 +
3.400 + # If the request refers to an older version of the object, ignore
3.401 + # it.
3.402 +
3.403 + return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp,
3.404 + self.is_partstat_updated(obj))
3.405 +
3.406 + return True
3.407 +
3.408 + def is_partstat_updated(self, obj):
3.409 +
3.410 + """
3.411 + Return whether the participant status has been updated in the current
3.412 + object in comparison to the given 'obj'.
3.413 +
3.414 + NOTE: Some clients like Claws Mail erase time information from DTSTAMP
3.415 + NOTE: and make it invalid. Thus, such attendance information may also be
3.416 + NOTE: incorporated into any new object assessment.
3.417 + """
3.418 +
3.419 + old_attendees = uri_dict(obj.get_value_map("ATTENDEE"))
3.420 + new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
3.421 +
3.422 + for attendee, attr in old_attendees.items():
3.423 + old_partstat = attr.get("PARTSTAT")
3.424 + new_attr = new_attendees.get(attendee)
3.425 + new_partstat = new_attr and new_attr.get("PARTSTAT")
3.426 +
3.427 + if old_partstat == "NEEDS-ACTION" and new_partstat and \
3.428 + new_partstat != old_partstat:
3.429 +
3.430 + return True
3.431 +
3.432 + return False
3.433 +
3.434 + def merge_attendance(self, attendees, identity):
3.435 +
3.436 + """
3.437 + Merge attendance from the current object's 'attendees' into the version
3.438 + stored for the given 'identity'.
3.439 + """
3.440 +
3.441 + obj = self.get_object(identity)
3.442 +
3.443 + if not obj or not self.have_new_object(identity, obj=obj):
3.444 + return False
3.445 +
3.446 + # Get attendee details in a usable form.
3.447 +
3.448 + attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
3.449 +
3.450 + for attendee, attendee_attr in attendees.items():
3.451 +
3.452 + # Update attendance in the loaded object.
3.453 +
3.454 + attendee_map[attendee] = attendee_attr
3.455 +
3.456 + # Set the new details and store the object.
3.457 +
3.458 + obj["ATTENDEE"] = attendee_map.items()
3.459 +
3.460 + # Set the complete event if not an additional occurrence.
3.461 +
3.462 + event = obj.to_node()
3.463 + recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
3.464 +
3.465 + self.store.set_event(identity, self.uid, self.recurrenceid, event)
3.466 +
3.467 + return True
3.468 +
3.469 + def update_dtstamp(self):
3.470 +
3.471 + "Update the DTSTAMP in the current object."
3.472 +
3.473 + dtstamp = self.obj.get_utc_datetime("DTSTAMP")
3.474 + utcnow = to_timezone(datetime.utcnow(), "UTC")
3.475 + self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})]
3.476 +
3.477 + def set_sequence(self, increment=False):
3.478 +
3.479 + "Update the SEQUENCE in the current object."
3.480 +
3.481 + sequence = self.obj.get_value("SEQUENCE") or "0"
3.482 + self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]
3.483 +
3.484 + def get_tzid(self, identity):
3.485 +
3.486 + "Return the time regime applicable for the given 'identity'."
3.487 +
3.488 + preferences = Preferences(identity)
3.489 + return preferences.get("TZID") or get_default_timezone()
3.490 +
3.491 +# vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/imiptools/handlers/person.py Sun Mar 22 18:36:34 2015 +0100
4.2 +++ b/imiptools/handlers/person.py Sun Mar 22 18:37:13 2015 +0100
4.3 @@ -19,9 +19,9 @@
4.4 this program. If not, see <http://www.gnu.org/licenses/>.
4.5 """
4.6
4.7 -from imiptools.content import Handler
4.8 from imiptools.data import get_uri
4.9 from imiptools.dates import format_datetime
4.10 +from imiptools.handlers import Handler
4.11 from imiptools.handlers.common import CommonFreebusy
4.12 from imiptools.period import replace_overlapping
4.13 from imiptools.profile import Preferences
5.1 --- a/imiptools/handlers/person_outgoing.py Sun Mar 22 18:36:34 2015 +0100
5.2 +++ b/imiptools/handlers/person_outgoing.py Sun Mar 22 18:37:13 2015 +0100
5.3 @@ -20,8 +20,8 @@
5.4 this program. If not, see <http://www.gnu.org/licenses/>.
5.5 """
5.6
5.7 -from imiptools.content import Handler
5.8 from imiptools.data import get_window_end, uri_dict, uri_item, uri_values
5.9 +from imiptools.handlers import Handler
5.10 from imiptools.period import remove_affected_period
5.11
5.12 class PersonHandler(Handler):
6.1 --- a/imiptools/handlers/resource.py Sun Mar 22 18:36:34 2015 +0100
6.2 +++ b/imiptools/handlers/resource.py Sun Mar 22 18:37:13 2015 +0100
6.3 @@ -19,9 +19,9 @@
6.4 this program. If not, see <http://www.gnu.org/licenses/>.
6.5 """
6.6
6.7 -from imiptools.content import Handler
6.8 from imiptools.data import get_address, get_uri, get_window_end, to_part
6.9 from imiptools.dates import get_default_timezone
6.10 +from imiptools.handlers import Handler
6.11 from imiptools.handlers.common import CommonFreebusy
6.12 from imiptools.period import remove_affected_period
6.13