# HG changeset patch # User Paul Boddie # Date 1427045833 -3600 # Node ID 674485854ec080e15b27a05cc08a17b18d490d8d # Parent cb30cdfe54b028099b030676dbd88b33fbd1c290 Moved the base Handler class into the handlers module. diff -r cb30cdfe54b0 -r 674485854ec0 imip_manager.py --- a/imip_manager.py Sun Mar 22 18:36:34 2015 +0100 +++ b/imip_manager.py Sun Mar 22 18:37:13 2015 +0100 @@ -31,7 +31,6 @@ sys.path.append(LIBRARY_PATH) -from imiptools.content import Handler from imiptools.data import get_address, get_uri, get_window_end, make_freebusy, \ Object, to_part, \ uri_dict, uri_item, uri_items, uri_values @@ -39,6 +38,7 @@ get_datetime_item, get_default_timezone, \ get_end_of_day, get_start_of_day, get_start_of_next_day, \ get_timestamp, ends_on_same_day, to_timezone +from imiptools.handlers import Handler from imiptools.mail import Messenger from imiptools.period import add_day_start_points, add_empty_days, add_slots, \ convert_periods, get_freebusy_details, \ diff -r cb30cdfe54b0 -r 674485854ec0 imiptools/content.py --- a/imiptools/content.py Sun Mar 22 18:36:34 2015 +0100 +++ b/imiptools/content.py Sun Mar 22 18:37:13 2015 +0100 @@ -20,19 +20,7 @@ this program. If not, see . """ -from datetime import datetime, timedelta -from email.mime.text import MIMEText -from imiptools.config import MANAGER_PATH, MANAGER_URL -from imiptools.data import Object, parse_object, \ - get_address, get_uri, get_value, get_window_end, \ - is_new_object, uri_dict, uri_item, uri_values -from imiptools.dates import format_datetime, get_default_timezone, to_timezone -from imiptools.period import can_schedule, insert_period, remove_period, \ - remove_additional_periods, remove_affected_period, \ - update_freebusy -from imiptools.profile import Preferences -from socket import gethostname -import imip_store +from imiptools.data import Object, parse_object, get_value try: from cStringIO import StringIO @@ -86,458 +74,6 @@ handler.set_object(Object({name : item})) methods[method](handler)() -# References to the Web interface. - -def get_manager_url(): - url_base = MANAGER_URL or "http://%s/" % gethostname() - return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/")) - -def get_object_url(uid, recurrenceid=None): - return "%s/%s%s" % ( - get_manager_url().rstrip("/"), uid, - recurrenceid and "/%s" % recurrenceid or "" - ) - -class Handler: - - "General handler support." - - def __init__(self, senders=None, recipient=None, messenger=None): - - """ - Initialise the handler with the calendar 'obj' and the 'senders' and - 'recipient' of the object (if specifically indicated). - """ - - self.senders = senders and set(map(get_address, senders)) - self.recipient = recipient and get_address(recipient) - self.messenger = messenger - - self.results = [] - self.outgoing_methods = set() - - self.obj = None - self.uid = None - self.recurrenceid = None - self.sequence = None - self.dtstamp = None - - self.store = imip_store.FileStore() - - try: - self.publisher = imip_store.FilePublisher() - except OSError: - self.publisher = None - - def set_object(self, obj): - self.obj = obj - self.uid = self.obj.get_value("UID") - self.recurrenceid = format_datetime(self.obj.get_utc_datetime("RECURRENCE-ID")) - self.sequence = self.obj.get_value("SEQUENCE") - self.dtstamp = self.obj.get_value("DTSTAMP") - - def wrap(self, text, link=True): - - "Wrap any valid message for passing to the recipient." - - texts = [] - texts.append(text) - if link: - texts.append("If your mail program cannot handle this " - "message, you may view the details here:\n\n%s" % - get_object_url(self.uid, self.recurrenceid)) - - return self.add_result(None, None, MIMEText("\n".join(texts))) - - # Result registration. - - def add_result(self, method, outgoing_recipients, part): - - """ - Record a result having the given 'method', 'outgoing_recipients' and - message part. - """ - - if outgoing_recipients: - self.outgoing_methods.add(method) - self.results.append((outgoing_recipients, part)) - - def get_results(self): - return self.results - - def get_outgoing_methods(self): - return self.outgoing_methods - - # Convenience methods for modifying free/busy collections. - - def remove_from_freebusy(self, freebusy): - - "Remove this event from the given 'freebusy' collection." - - remove_period(freebusy, self.uid, self.recurrenceid) - - def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): - - """ - Remove from 'freebusy' any original recurrence from parent free/busy - details for the current object, if the current object is a specific - additional recurrence. Otherwise, remove all additional recurrence - information corresponding to 'recurrenceids', or if omitted, all - recurrences. - """ - - if self.recurrenceid: - remove_affected_period(freebusy, self.uid, self.recurrenceid) - else: - # Remove obsolete recurrence periods. - - remove_additional_periods(freebusy, self.uid, recurrenceids) - - # Remove original periods affected by additional recurrences. - - if recurrenceids: - for recurrenceid in recurrenceids: - remove_affected_period(freebusy, self.uid, recurrenceid) - - def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None): - - """ - 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, - transp or self.obj.get_value("TRANSP"), - self.uid, recurrenceid, - self.obj.get_value("SUMMARY"), - self.obj.get_value("ORGANIZER")) - - def update_freebusy(self, freebusy, periods, transp=None): - - """ - Update the 'freebusy' collection for this event with the given - 'periods'. - """ - - self._update_freebusy(freebusy, periods, self.recurrenceid, transp) - - def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False): - - """ - Update the 'freebusy' collection using the given 'periods', subject to - the 'attr' provided for the participant, indicating whether this is - being generated 'for_organiser' or not. - """ - - # Organisers employ a special transparency. - - if for_organiser or attr.get("PARTSTAT") != "DECLINED": - self.update_freebusy(freebusy, periods, transp=(for_organiser and "ORG" or None)) - else: - self.remove_from_freebusy(freebusy) - - # Convenience methods for updating stored free/busy information. - - def update_freebusy_from_participant(self, user, participant_item, for_organiser): - - """ - For the given 'user', record the free/busy information for the - 'participant_item' (a value plus attributes) representing a different - identity, thus maintaining a separate record of their free/busy details. - """ - - participant, participant_attr = participant_item - - if participant == user: - return - - freebusy = self.store.get_freebusy_for_other(user, participant) - tzid = self.get_tzid(user) - window_end = get_window_end(tzid) - periods = self.obj.get_periods_for_freebusy(tzid, window_end) - - # Record in the free/busy details unless a non-participating attendee. - - self.update_freebusy_for_participant(freebusy, periods, participant_attr, - for_organiser and self.is_not_attendee(participant, self.obj)) - - self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(user, self.uid)) - self.store.set_freebusy_for_other(user, freebusy, participant) - - def update_freebusy_from_organiser(self, attendee, organiser_item): - - """ - For the 'attendee', record free/busy information from the - 'organiser_item' (a value plus attributes). - """ - - self.update_freebusy_from_participant(attendee, organiser_item, True) - - def update_freebusy_from_attendees(self, organiser, attendees): - - "For the 'organiser', record free/busy information from 'attendees'." - - for attendee_item in attendees.items(): - self.update_freebusy_from_participant(organiser, attendee_item, False) - - # Logic, filtering and access to calendar structures and other data. - - def is_not_attendee(self, identity, obj): - - "Return whether 'identity' is not an attendee in 'obj'." - - return identity not in uri_values(obj.get_values("ATTENDEE")) - - def can_schedule(self, freebusy, periods): - return can_schedule(freebusy, periods, self.uid, self.recurrenceid) - - def filter_by_senders(self, mapping): - - """ - Return a list of items from 'mapping' filtered using sender information. - """ - - if self.senders: - - # Get a mapping from senders to identities. - - identities = self.get_sender_identities(mapping) - - # Find the senders that are valid. - - senders = map(get_address, identities) - valid = self.senders.intersection(senders) - - # Return the true identities. - - return [identities[get_uri(address)] for address in valid] - else: - return mapping - - def filter_by_recipient(self, mapping): - - """ - Return a list of items from 'mapping' filtered using recipient - information. - """ - - if self.recipient: - addresses = set(map(get_address, mapping)) - return map(get_uri, addresses.intersection([self.recipient])) - else: - return mapping - - def require_organiser(self, from_organiser=True): - - """ - Return the organiser for the current object, filtered for the sender or - recipient of interest. Return None if no identities are eligible. - - The organiser identity is normalized. - """ - - organiser_item = uri_item(self.obj.get_item("ORGANIZER")) - - # Only provide details for an organiser who sent/receives the message. - - organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient - - if not organiser_filter_fn(dict([organiser_item])): - return None - - return organiser_item - - def require_attendees(self, from_organiser=True): - - """ - Return the attendees for the current object, filtered for the sender or - recipient of interest. Return None if no identities are eligible. - - The attendee identities are normalized. - """ - - attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) - - # Only provide details for attendees who sent/receive the message. - - attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders - - attendees = {} - for attendee in attendee_filter_fn(attendee_map): - attendees[attendee] = attendee_map[attendee] - - return attendees - - def require_organiser_and_attendees(self, from_organiser=True): - - """ - Return the organiser and attendees for the current object, filtered for - the recipient of interest. Return None if no identities are eligible. - - Organiser and attendee identities are normalized. - """ - - organiser_item = self.require_organiser(from_organiser) - attendees = self.require_attendees(from_organiser) - - if not attendees or not organiser_item: - return None - - return organiser_item, attendees - - def get_sender_identities(self, mapping): - - """ - Return a mapping from actual senders to the identities for which they - have provided data, extracting this information from the given - 'mapping'. - """ - - senders = {} - - for value, attr in mapping.items(): - sent_by = attr.get("SENT-BY") - if sent_by: - senders[get_uri(sent_by)] = value - else: - senders[value] = value - - return senders - - def _get_object(self, user, uid, recurrenceid): - - """ - Return the stored object for the given 'user', 'uid' and 'recurrenceid'. - """ - - fragment = self.store.get_event(user, uid, recurrenceid) - return fragment and Object(fragment) - - def get_object(self, user): - - """ - Return the stored object to which the current object refers for the - given 'user'. - """ - - return self._get_object(user, self.uid, self.recurrenceid) - - def get_parent_object(self, user): - - """ - Return the parent object to which the current object refers for the - given 'user'. - """ - - return self.recurrenceid and self._get_object(user, self.uid, None) or None - - def have_new_object(self, attendee, obj=None): - - """ - Return whether the current object is new to the 'attendee' (or if the - given 'obj' is new). - """ - - obj = obj or self.get_object(attendee) - - # If found, compare SEQUENCE and potentially DTSTAMP. - - if obj: - sequence = obj.get_value("SEQUENCE") - dtstamp = obj.get_value("DTSTAMP") - - # If the request refers to an older version of the object, ignore - # it. - - return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, - self.is_partstat_updated(obj)) - - return True - - def is_partstat_updated(self, obj): - - """ - Return whether the participant status has been updated in the current - object in comparison to the given 'obj'. - - NOTE: Some clients like Claws Mail erase time information from DTSTAMP - NOTE: and make it invalid. Thus, such attendance information may also be - NOTE: incorporated into any new object assessment. - """ - - old_attendees = uri_dict(obj.get_value_map("ATTENDEE")) - new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE")) - - for attendee, attr in old_attendees.items(): - old_partstat = attr.get("PARTSTAT") - new_attr = new_attendees.get(attendee) - new_partstat = new_attr and new_attr.get("PARTSTAT") - - if old_partstat == "NEEDS-ACTION" and new_partstat and \ - new_partstat != old_partstat: - - return True - - return False - - def merge_attendance(self, attendees, identity): - - """ - Merge attendance from the current object's 'attendees' into the version - stored for the given 'identity'. - """ - - obj = self.get_object(identity) - - if not obj or not self.have_new_object(identity, obj=obj): - return False - - # Get attendee details in a usable form. - - attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) - - for attendee, attendee_attr in attendees.items(): - - # Update attendance in the loaded object. - - attendee_map[attendee] = attendee_attr - - # Set the new details and store the object. - - obj["ATTENDEE"] = attendee_map.items() - - # Set the complete event if not an additional occurrence. - - event = obj.to_node() - recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID")) - - self.store.set_event(identity, self.uid, self.recurrenceid, event) - - return True - - def update_dtstamp(self): - - "Update the DTSTAMP in the current object." - - dtstamp = self.obj.get_utc_datetime("DTSTAMP") - utcnow = to_timezone(datetime.utcnow(), "UTC") - self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})] - - def set_sequence(self, increment=False): - - "Update the SEQUENCE in the current object." - - sequence = self.obj.get_value("SEQUENCE") or "0" - self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] - - 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 cb30cdfe54b0 -r 674485854ec0 imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Sun Mar 22 18:36:34 2015 +0100 +++ b/imiptools/handlers/__init__.py Sun Mar 22 18:37:13 2015 +0100 @@ -0,0 +1,488 @@ +#!/usr/bin/env python + +""" +General handler support for incoming calendar objects. + +Copyright (C) 2014, 2015 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from datetime import datetime +from email.mime.text import MIMEText +from imiptools.config import MANAGER_PATH, MANAGER_URL +from imiptools.data import Object, \ + get_address, get_uri, get_value, get_window_end, \ + is_new_object, uri_dict, uri_item, uri_values +from imiptools.dates import format_datetime, get_default_timezone, to_timezone +from imiptools.period import can_schedule, remove_period, \ + remove_additional_periods, remove_affected_period, \ + update_freebusy +from imiptools.profile import Preferences +from socket import gethostname +import imip_store + +# References to the Web interface. + +def get_manager_url(): + url_base = MANAGER_URL or "http://%s/" % gethostname() + return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/")) + +def get_object_url(uid, recurrenceid=None): + return "%s/%s%s" % ( + get_manager_url().rstrip("/"), uid, + recurrenceid and "/%s" % recurrenceid or "" + ) + +class Handler: + + "General handler support." + + def __init__(self, senders=None, recipient=None, messenger=None): + + """ + Initialise the handler with the calendar 'obj' and the 'senders' and + 'recipient' of the object (if specifically indicated). + """ + + self.senders = senders and set(map(get_address, senders)) + self.recipient = recipient and get_address(recipient) + self.messenger = messenger + + self.results = [] + self.outgoing_methods = set() + + self.obj = None + self.uid = None + self.recurrenceid = None + self.sequence = None + self.dtstamp = None + + self.store = imip_store.FileStore() + + try: + self.publisher = imip_store.FilePublisher() + except OSError: + self.publisher = None + + def set_object(self, obj): + self.obj = obj + self.uid = self.obj.get_value("UID") + self.recurrenceid = format_datetime(self.obj.get_utc_datetime("RECURRENCE-ID")) + self.sequence = self.obj.get_value("SEQUENCE") + self.dtstamp = self.obj.get_value("DTSTAMP") + + def wrap(self, text, link=True): + + "Wrap any valid message for passing to the recipient." + + texts = [] + texts.append(text) + if link: + texts.append("If your mail program cannot handle this " + "message, you may view the details here:\n\n%s" % + get_object_url(self.uid, self.recurrenceid)) + + return self.add_result(None, None, MIMEText("\n".join(texts))) + + # Result registration. + + def add_result(self, method, outgoing_recipients, part): + + """ + Record a result having the given 'method', 'outgoing_recipients' and + message part. + """ + + if outgoing_recipients: + self.outgoing_methods.add(method) + self.results.append((outgoing_recipients, part)) + + def get_results(self): + return self.results + + def get_outgoing_methods(self): + return self.outgoing_methods + + # Convenience methods for modifying free/busy collections. + + def remove_from_freebusy(self, freebusy): + + "Remove this event from the given 'freebusy' collection." + + remove_period(freebusy, self.uid, self.recurrenceid) + + def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): + + """ + Remove from 'freebusy' any original recurrence from parent free/busy + details for the current object, if the current object is a specific + additional recurrence. Otherwise, remove all additional recurrence + information corresponding to 'recurrenceids', or if omitted, all + recurrences. + """ + + if self.recurrenceid: + remove_affected_period(freebusy, self.uid, self.recurrenceid) + else: + # Remove obsolete recurrence periods. + + remove_additional_periods(freebusy, self.uid, recurrenceids) + + # Remove original periods affected by additional recurrences. + + if recurrenceids: + for recurrenceid in recurrenceids: + remove_affected_period(freebusy, self.uid, recurrenceid) + + def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None): + + """ + 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, + transp or self.obj.get_value("TRANSP"), + self.uid, recurrenceid, + self.obj.get_value("SUMMARY"), + self.obj.get_value("ORGANIZER")) + + def update_freebusy(self, freebusy, periods, transp=None): + + """ + Update the 'freebusy' collection for this event with the given + 'periods'. + """ + + self._update_freebusy(freebusy, periods, self.recurrenceid, transp) + + def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False): + + """ + Update the 'freebusy' collection using the given 'periods', subject to + the 'attr' provided for the participant, indicating whether this is + being generated 'for_organiser' or not. + """ + + # Organisers employ a special transparency. + + if for_organiser or attr.get("PARTSTAT") != "DECLINED": + self.update_freebusy(freebusy, periods, transp=(for_organiser and "ORG" or None)) + else: + self.remove_from_freebusy(freebusy) + + # Convenience methods for updating stored free/busy information. + + def update_freebusy_from_participant(self, user, participant_item, for_organiser): + + """ + For the given 'user', record the free/busy information for the + 'participant_item' (a value plus attributes) representing a different + identity, thus maintaining a separate record of their free/busy details. + """ + + participant, participant_attr = participant_item + + if participant == user: + return + + freebusy = self.store.get_freebusy_for_other(user, participant) + tzid = self.get_tzid(user) + window_end = get_window_end(tzid) + periods = self.obj.get_periods_for_freebusy(tzid, window_end) + + # Record in the free/busy details unless a non-participating attendee. + + self.update_freebusy_for_participant(freebusy, periods, participant_attr, + for_organiser and self.is_not_attendee(participant, self.obj)) + + self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(user, self.uid)) + self.store.set_freebusy_for_other(user, freebusy, participant) + + def update_freebusy_from_organiser(self, attendee, organiser_item): + + """ + For the 'attendee', record free/busy information from the + 'organiser_item' (a value plus attributes). + """ + + self.update_freebusy_from_participant(attendee, organiser_item, True) + + def update_freebusy_from_attendees(self, organiser, attendees): + + "For the 'organiser', record free/busy information from 'attendees'." + + for attendee_item in attendees.items(): + self.update_freebusy_from_participant(organiser, attendee_item, False) + + # Logic, filtering and access to calendar structures and other data. + + def is_not_attendee(self, identity, obj): + + "Return whether 'identity' is not an attendee in 'obj'." + + return identity not in uri_values(obj.get_values("ATTENDEE")) + + def can_schedule(self, freebusy, periods): + return can_schedule(freebusy, periods, self.uid, self.recurrenceid) + + def filter_by_senders(self, mapping): + + """ + Return a list of items from 'mapping' filtered using sender information. + """ + + if self.senders: + + # Get a mapping from senders to identities. + + identities = self.get_sender_identities(mapping) + + # Find the senders that are valid. + + senders = map(get_address, identities) + valid = self.senders.intersection(senders) + + # Return the true identities. + + return [identities[get_uri(address)] for address in valid] + else: + return mapping + + def filter_by_recipient(self, mapping): + + """ + Return a list of items from 'mapping' filtered using recipient + information. + """ + + if self.recipient: + addresses = set(map(get_address, mapping)) + return map(get_uri, addresses.intersection([self.recipient])) + else: + return mapping + + def require_organiser(self, from_organiser=True): + + """ + Return the organiser for the current object, filtered for the sender or + recipient of interest. Return None if no identities are eligible. + + The organiser identity is normalized. + """ + + organiser_item = uri_item(self.obj.get_item("ORGANIZER")) + + # Only provide details for an organiser who sent/receives the message. + + organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient + + if not organiser_filter_fn(dict([organiser_item])): + return None + + return organiser_item + + def require_attendees(self, from_organiser=True): + + """ + Return the attendees for the current object, filtered for the sender or + recipient of interest. Return None if no identities are eligible. + + The attendee identities are normalized. + """ + + attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) + + # Only provide details for attendees who sent/receive the message. + + attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders + + attendees = {} + for attendee in attendee_filter_fn(attendee_map): + attendees[attendee] = attendee_map[attendee] + + return attendees + + def require_organiser_and_attendees(self, from_organiser=True): + + """ + Return the organiser and attendees for the current object, filtered for + the recipient of interest. Return None if no identities are eligible. + + Organiser and attendee identities are normalized. + """ + + organiser_item = self.require_organiser(from_organiser) + attendees = self.require_attendees(from_organiser) + + if not attendees or not organiser_item: + return None + + return organiser_item, attendees + + def get_sender_identities(self, mapping): + + """ + Return a mapping from actual senders to the identities for which they + have provided data, extracting this information from the given + 'mapping'. + """ + + senders = {} + + for value, attr in mapping.items(): + sent_by = attr.get("SENT-BY") + if sent_by: + senders[get_uri(sent_by)] = value + else: + senders[value] = value + + return senders + + def _get_object(self, user, uid, recurrenceid): + + """ + Return the stored object for the given 'user', 'uid' and 'recurrenceid'. + """ + + fragment = self.store.get_event(user, uid, recurrenceid) + return fragment and Object(fragment) + + def get_object(self, user): + + """ + Return the stored object to which the current object refers for the + given 'user'. + """ + + return self._get_object(user, self.uid, self.recurrenceid) + + def get_parent_object(self, user): + + """ + Return the parent object to which the current object refers for the + given 'user'. + """ + + return self.recurrenceid and self._get_object(user, self.uid, None) or None + + def have_new_object(self, attendee, obj=None): + + """ + Return whether the current object is new to the 'attendee' (or if the + given 'obj' is new). + """ + + obj = obj or self.get_object(attendee) + + # If found, compare SEQUENCE and potentially DTSTAMP. + + if obj: + sequence = obj.get_value("SEQUENCE") + dtstamp = obj.get_value("DTSTAMP") + + # If the request refers to an older version of the object, ignore + # it. + + return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, + self.is_partstat_updated(obj)) + + return True + + def is_partstat_updated(self, obj): + + """ + Return whether the participant status has been updated in the current + object in comparison to the given 'obj'. + + NOTE: Some clients like Claws Mail erase time information from DTSTAMP + NOTE: and make it invalid. Thus, such attendance information may also be + NOTE: incorporated into any new object assessment. + """ + + old_attendees = uri_dict(obj.get_value_map("ATTENDEE")) + new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE")) + + for attendee, attr in old_attendees.items(): + old_partstat = attr.get("PARTSTAT") + new_attr = new_attendees.get(attendee) + new_partstat = new_attr and new_attr.get("PARTSTAT") + + if old_partstat == "NEEDS-ACTION" and new_partstat and \ + new_partstat != old_partstat: + + return True + + return False + + def merge_attendance(self, attendees, identity): + + """ + Merge attendance from the current object's 'attendees' into the version + stored for the given 'identity'. + """ + + obj = self.get_object(identity) + + if not obj or not self.have_new_object(identity, obj=obj): + return False + + # Get attendee details in a usable form. + + attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) + + for attendee, attendee_attr in attendees.items(): + + # Update attendance in the loaded object. + + attendee_map[attendee] = attendee_attr + + # Set the new details and store the object. + + obj["ATTENDEE"] = attendee_map.items() + + # Set the complete event if not an additional occurrence. + + event = obj.to_node() + recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID")) + + self.store.set_event(identity, self.uid, self.recurrenceid, event) + + return True + + def update_dtstamp(self): + + "Update the DTSTAMP in the current object." + + dtstamp = self.obj.get_utc_datetime("DTSTAMP") + utcnow = to_timezone(datetime.utcnow(), "UTC") + self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})] + + def set_sequence(self, increment=False): + + "Update the SEQUENCE in the current object." + + sequence = self.obj.get_value("SEQUENCE") or "0" + self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] + + 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() + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r cb30cdfe54b0 -r 674485854ec0 imiptools/handlers/person.py --- a/imiptools/handlers/person.py Sun Mar 22 18:36:34 2015 +0100 +++ b/imiptools/handlers/person.py Sun Mar 22 18:37:13 2015 +0100 @@ -19,9 +19,9 @@ this program. If not, see . """ -from imiptools.content import Handler from imiptools.data import get_uri from imiptools.dates import format_datetime +from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy from imiptools.period import replace_overlapping from imiptools.profile import Preferences diff -r cb30cdfe54b0 -r 674485854ec0 imiptools/handlers/person_outgoing.py --- a/imiptools/handlers/person_outgoing.py Sun Mar 22 18:36:34 2015 +0100 +++ b/imiptools/handlers/person_outgoing.py Sun Mar 22 18:37:13 2015 +0100 @@ -20,8 +20,8 @@ this program. If not, see . """ -from imiptools.content import Handler from imiptools.data import get_window_end, uri_dict, uri_item, uri_values +from imiptools.handlers import Handler from imiptools.period import remove_affected_period class PersonHandler(Handler): diff -r cb30cdfe54b0 -r 674485854ec0 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Sun Mar 22 18:36:34 2015 +0100 +++ b/imiptools/handlers/resource.py Sun Mar 22 18:37:13 2015 +0100 @@ -19,9 +19,9 @@ this program. If not, see . """ -from imiptools.content import Handler from imiptools.data import get_address, get_uri, get_window_end, to_part from imiptools.dates import get_default_timezone +from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy from imiptools.period import remove_affected_period