# HG changeset patch # User Paul Boddie # Date 1437859138 -7200 # Node ID f6cad1ec5673c8cd74ebcc75abc94c6f5286a402 # Parent 214be3077eff0c6e4cb5baed848f3a0b48b0de5e Introduced a separate client-related abstraction involving a specific object for each instance, of which a handler is a specialisation, but at a level more appropriate for manager and test purposes than a handler (which is really intended to support object-specific handling methods as part of handling a message in transit). diff -r 214be3077eff -r f6cad1ec5673 imiptools/client.py --- a/imiptools/client.py Sat Jul 25 19:29:44 2015 +0200 +++ b/imiptools/client.py Sat Jul 25 23:18:58 2015 +0200 @@ -19,9 +19,12 @@ this program. If not, see . """ +from datetime import datetime from imiptools.data import get_address, get_uri, get_window_end, uri_dict, uri_items, uri_values -from imiptools.dates import get_default_timezone +from imiptools.period import update_freebusy from imiptools.profile import Preferences +from imiptools.dates import format_datetime, get_default_timezone, \ + to_timezone def update_attendees(obj, attendees, removed): @@ -72,8 +75,9 @@ default_window_size = 100 - def __init__(self, user): + def __init__(self, user, messenger=None): self.user = user + self.messenger = messenger self.preferences = None def get_preferences(self): @@ -132,4 +136,79 @@ if self.messenger and self.messenger.sender != get_address(self.user): attr["SENT-BY"] = get_uri(self.messenger.sender) +class ClientForObject(Client): + + "A client maintaining a specific object." + + def __init__(self, obj, user, messenger=None): + Client.__init__(self, user, messenger) + self.set_object(obj) + + def set_object(self, obj): + self.obj = obj + self.uid = obj and self.obj.get_uid() + self.recurrenceid = obj and self.obj.get_recurrenceid() + self.sequence = obj and self.obj.get_value("SEQUENCE") + self.dtstamp = obj and self.obj.get_value("DTSTAMP") + + 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") or "OPAQUE", + 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 not attending. + + if self.is_participating(attr, for_organiser): + self.update_freebusy(freebusy, periods, + transp=self.get_overriding_transparency(attr, for_organiser)) + else: + self.remove_from_freebusy(freebusy) + + def is_participating(self, attr, as_organiser=False): + return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED" + + def get_overriding_transparency(self, attr, as_organiser=False): + return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None + + 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)), {})] + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 214be3077eff -r f6cad1ec5673 imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Sat Jul 25 19:29:44 2015 +0200 +++ b/imiptools/handlers/__init__.py Sat Jul 25 23:18:58 2015 +0200 @@ -19,18 +19,15 @@ this program. If not, see . """ -from datetime import datetime from email.mime.text import MIMEText -from imiptools.client import Client +from imiptools.client import ClientForObject from imiptools.config import MANAGER_PATH, MANAGER_URL -from imiptools.data import Object, \ - get_address, get_uri, get_value, \ +from imiptools.data import Object, get_address, get_uri, \ is_new_object, uri_dict, uri_item, uri_values from imiptools.dates import format_datetime, get_recurrence_start_point, \ to_timezone from imiptools.period import can_schedule, remove_period, \ - remove_additional_periods, remove_affected_period, \ - update_freebusy + remove_additional_periods, remove_affected_period from imiptools.profile import Preferences from socket import gethostname import imip_store @@ -47,7 +44,7 @@ recurrenceid and "/%s" % recurrenceid or "" ) -class Handler(Client): +class Handler(ClientForObject): "General handler support." @@ -55,28 +52,24 @@ publisher=None): """ - Initialise the handler with the calendar 'obj' and the 'senders' and - 'recipient' of the object (if specifically indicated). + Initialise the handler with any specifically indicated 'senders' and + 'recipient' of a calendar object. The object is initially undefined. + + The optional 'messenger' provides a means of interacting with the mail + system. The optional 'store' and 'publisher' can be specified to override the default store and publisher objects. """ - Client.__init__(self, recipient and get_uri(recipient)) + ClientForObject.__init__(self, None, recipient and get_uri(recipient), messenger) 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 = store or imip_store.FileStore() try: @@ -84,13 +77,6 @@ except OSError: self.publisher = None - def set_object(self, obj): - self.obj = obj - self.uid = self.obj.get_uid() - self.recurrenceid = self.obj.get_recurrenceid() - 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." @@ -164,51 +150,6 @@ recurrenceid = self.get_recurrence_start_point(recurrenceid) 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") or "OPAQUE", - 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 not attending. - - if self.is_participating(attr, for_organiser): - self.update_freebusy(freebusy, periods, - transp=self.get_overriding_transparency(attr, for_organiser)) - else: - self.remove_from_freebusy(freebusy) - - def is_participating(self, attr, as_organiser=False): - return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED" - - def get_overriding_transparency(self, attr, as_organiser=False): - return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None - # Convenience methods for updating stored free/busy information. def update_freebusy_from_participant(self, participant_item, for_organiser): @@ -523,19 +464,4 @@ 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)), {})] - # vim: tabstop=4 expandtab shiftwidth=4 diff -r 214be3077eff -r f6cad1ec5673 imipweb/client.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imipweb/client.py Sat Jul 25 23:18:58 2015 +0200 @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +""" +Interaction with the mail system for the manager interface. + +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 imiptools.client import ClientForObject +from imiptools.data import get_address, get_uri, make_freebusy, \ + to_part, uri_item, uri_values +from imiptools.dates import format_datetime, get_timestamp + +class ManagerClient(ClientForObject): + + """ + A content client for use by the manager, as opposed to operating within the + mail processing pipeline. + """ + + # Communication methods. + + def send_message(self, method, sender, from_organiser, parts=None): + + """ + Create a full calendar object employing the given 'method', and send it + to the appropriate recipients, also sending a copy to the 'sender'. The + 'from_organiser' value indicates whether the organiser is sending this + message. + """ + + parts = parts or [self.obj.to_part(method)] + + # As organiser, send an invitation to attendees, excluding oneself if + # also attending. The updated event will be saved by the outgoing + # handler. + + organiser = get_uri(self.obj.get_value("ORGANIZER")) + attendees = uri_values(self.obj.get_values("ATTENDEE")) + + if from_organiser: + recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] + else: + recipients = [get_address(organiser)] + + # Bundle free/busy information if appropriate. + + if self.is_sharing() and self.is_bundling(): + + # Invent a unique identifier. + + utcnow = get_timestamp() + uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) + + freebusy = self.store.get_freebusy(self.user) + + # Since the outgoing handler updates this user's free/busy details, + # the stored details will probably not have the updated details at + # this point, so we update our copy for serialisation as the bundled + # free/busy object. + + self.update_freebusy(freebusy, + self.obj.get_periods(self.get_tzid(), self.get_window_end())) + + user_attr = {} + self.update_sender(user_attr) + + parts.append(to_part("PUBLISH", [ + make_freebusy(freebusy, uid, self.user, user_attr) + ])) + + # Explicitly specify the outgoing BCC recipient since we are sending as + # the generic calendar user. + + message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) + self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) + + # Action methods. + + def process_received_request(self): + + """ + Process the current request for the current user. Return whether any + action was taken. + """ + + # Reply only on behalf of this user. + + attendee_attr = self.update_participation(self.obj) + + if not attendee_attr: + return False + + self.obj["ATTENDEE"] = [(self.user, attendee_attr)] + self.update_dtstamp() + self.set_sequence(False) + self.send_message("REPLY", get_address(self.user), from_organiser=False) + return True + + def process_created_request(self, method, to_cancel=None, to_unschedule=None): + + """ + Process the current request, sending a created request of the given + 'method' to attendees. Return whether any action was taken. + + If 'to_cancel' is specified, a list of participants to be sent cancel + messages is provided. + """ + + # Here, the organiser should be the current user. + + organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER")) + + self.update_sender(organiser_attr) + self.update_dtstamp() + self.set_sequence(True) + + parts = [self.obj.to_part(method)] + + # Add message parts with cancelled occurrence information. + # NOTE: This could probably be merged with the updated event message. + + if to_unschedule: + obj = self.obj.copy() + obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) + + for p in to_unschedule: + if not p.origin: + continue + obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), {})] + parts.append(obj.to_part("CANCEL")) + + # Send the updated event, along with a cancellation for each of the + # unscheduled occurrences. + + self.send_message("CANCEL", get_address(organiser), from_organiser=True, parts=parts) + + # When cancelling, replace the attendees with those for whom the event + # is now cancelled. + + if to_cancel: + obj = self.obj.copy() + obj["ATTENDEE"] = to_cancel + + # Send a cancellation to all uninvited attendees. + + self.send_message("CANCEL", get_address(organiser), from_organiser=True) + + return True + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 214be3077eff -r f6cad1ec5673 imipweb/event.py --- a/imipweb/event.py Sat Jul 25 19:29:44 2015 +0200 +++ b/imipweb/event.py Sat Jul 25 23:18:58 2015 +0200 @@ -29,7 +29,7 @@ from imipweb.data import EventPeriod, \ event_period_from_period, form_period_from_period, \ FormDate, FormPeriod, PeriodError -from imipweb.handler import ManagerHandler +from imipweb.client import ManagerClient from imipweb.resource import Resource import pytz @@ -157,7 +157,7 @@ if reply or invite or cancel: - handler = ManagerHandler(obj, self.user, self.messenger) + handler = ManagerClient(obj, self.user, self.messenger) # Process the object and remove it from the list of requests. diff -r 214be3077eff -r f6cad1ec5673 imipweb/handler.py --- a/imipweb/handler.py Sat Jul 25 19:29:44 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,171 +0,0 @@ -#!/usr/bin/env python - -""" -Interaction with the mail system for the manager interface. - -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 imiptools.client import Client -from imiptools.data import get_address, get_uri, make_freebusy, \ - to_part, uri_item, uri_values -from imiptools.dates import format_datetime, get_timestamp -from imiptools.handlers import Handler - -class ManagerHandler(Handler): - - """ - A content handler for use by the manager, as opposed to operating within the - mail processing pipeline. - """ - - def __init__(self, obj, user, messenger): - Handler.__init__(self, messenger=messenger) - Client.__init__(self, user) # this redefines the Handler initialisation - - self.set_object(obj) - - # Communication methods. - - def send_message(self, method, sender, from_organiser, parts=None): - - """ - Create a full calendar object employing the given 'method', and send it - to the appropriate recipients, also sending a copy to the 'sender'. The - 'from_organiser' value indicates whether the organiser is sending this - message. - """ - - parts = parts or [self.obj.to_part(method)] - - # As organiser, send an invitation to attendees, excluding oneself if - # also attending. The updated event will be saved by the outgoing - # handler. - - organiser = get_uri(self.obj.get_value("ORGANIZER")) - attendees = uri_values(self.obj.get_values("ATTENDEE")) - - if from_organiser: - recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] - else: - recipients = [get_address(organiser)] - - # Bundle free/busy information if appropriate. - - if self.is_sharing() and self.is_bundling(): - - # Invent a unique identifier. - - utcnow = get_timestamp() - uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) - - freebusy = self.store.get_freebusy(self.user) - - # Since the outgoing handler updates this user's free/busy details, - # the stored details will probably not have the updated details at - # this point, so we update our copy for serialisation as the bundled - # free/busy object. - - self.update_freebusy(freebusy, - self.obj.get_periods(self.get_tzid(), self.get_window_end())) - - user_attr = {} - self.update_sender(user_attr) - - parts.append(to_part("PUBLISH", [ - make_freebusy(freebusy, uid, self.user, user_attr) - ])) - - # Explicitly specify the outgoing BCC recipient since we are sending as - # the generic calendar user. - - message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) - self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) - - # Action methods. - - def process_received_request(self): - - """ - Process the current request for the current user. Return whether any - action was taken. - """ - - # Reply only on behalf of this user. - - attendee_attr = self.update_participation(self.obj) - - if not attendee_attr: - return False - - self.obj["ATTENDEE"] = [(self.user, attendee_attr)] - self.update_dtstamp() - self.set_sequence(False) - self.send_message("REPLY", get_address(self.user), from_organiser=False) - return True - - def process_created_request(self, method, to_cancel=None, to_unschedule=None): - - """ - Process the current request, sending a created request of the given - 'method' to attendees. Return whether any action was taken. - - If 'to_cancel' is specified, a list of participants to be sent cancel - messages is provided. - """ - - # Here, the organiser should be the current user. - - organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER")) - - self.update_sender(organiser_attr) - self.update_dtstamp() - self.set_sequence(True) - - parts = [self.obj.to_part(method)] - - # Add message parts with cancelled occurrence information. - # NOTE: This could probably be merged with the updated event message. - - if to_unschedule: - obj = self.obj.copy() - obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) - - for p in to_unschedule: - if not p.origin: - continue - obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), {})] - parts.append(obj.to_part("CANCEL")) - - # Send the updated event, along with a cancellation for each of the - # unscheduled occurrences. - - self.send_message("CANCEL", get_address(organiser), from_organiser=True, parts=parts) - - # When cancelling, replace the attendees with those for whom the event - # is now cancelled. - - if to_cancel: - obj = self.obj.copy() - obj["ATTENDEE"] = to_cancel - - # Send a cancellation to all uninvited attendees. - - self.send_message("CANCEL", get_address(organiser), from_organiser=True) - - return True - -# vim: tabstop=4 expandtab shiftwidth=4 diff -r 214be3077eff -r f6cad1ec5673 tests/test_handle.py --- a/tests/test_handle.py Sat Jul 25 19:29:44 2015 +0200 +++ b/tests/test_handle.py Sat Jul 25 23:18:58 2015 +0200 @@ -19,26 +19,19 @@ this program. If not, see . """ -from imiptools.client import Client +from imiptools.client import ClientForObject from imiptools.data import Object, get_address -from imiptools.handlers import Handler from imiptools.mail import Messenger import imip_store import sys -class TestHandler(Handler): +class TestClient(ClientForObject): """ A content handler for use in testing, as opposed to operating within the mail processing pipeline. """ - def __init__(self, obj, user, messenger): - Handler.__init__(self, messenger=messenger) - Client.__init__(self, user) # this redefines the Handler initialisation - - self.set_object(obj) - # Action methods. def handle_request(self, accept): @@ -55,7 +48,7 @@ if not attendee_attr: return None - # NOTE: This is a simpler form of the code in imipweb.handler. + # NOTE: This is a simpler form of the code in imipweb.client. organiser = get_address(self.obj.get_value("ORGANIZER")) @@ -91,7 +84,7 @@ sys.exit(1) obj = Object(fragment) - handler = TestHandler(obj, user, Messenger()) + handler = TestClient(obj, user, Messenger()) response = handler.handle_request(accept == "accept") if response: