imip-agent

imiptools/handlers/person.py

760:79493ac5b434
2015-09-24 Paul Boddie Permit separate counter-proposals from different attendees.
     1 #!/usr/bin/env python     2      3 """     4 Handlers for a person for whom scheduling is performed.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from imiptools.data import get_address, to_part, uri_dict, uri_item    23 from imiptools.handlers import Handler    24 from imiptools.handlers.common import CommonFreebusy, CommonEvent    25 from imiptools.period import FreeBusyPeriod, Period, replace_overlapping    26     27 class PersonHandler(CommonEvent, Handler):    28     29     "Event handling mechanisms specific to people."    30     31     def _add(self, queue=True):    32     33         """    34         Add an event occurrence for the current object or produce a response    35         that requests the event details to be sent again.    36         """    37     38         # Obtain valid organiser and attendee details.    39     40         oa = self.require_organiser_and_attendees()    41         if not oa:    42             return False    43     44         (organiser, organiser_attr), attendees = oa    45     46         # Request details where configured, doing so for unknown objects anyway.    47     48         if self.will_refresh():    49             self.make_refresh()    50     51         # Record the event as a recurrence of the parent object.    52     53         self.update_recurrenceid()    54     55         # Update the recipient's record of the organiser's schedule.    56     57         self.update_freebusy_from_organiser(organiser)    58     59         # Stop if requesting the full event.    60     61         if self.will_refresh():    62             return    63     64         # Set the additional occurrence.    65     66         self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())    67     68         # Queue any request, if appropriate.    69     70         if queue:    71             self.store.queue_request(self.user, self.uid, self.recurrenceid)    72     73         return True    74     75     def _counter(self):    76     77         """    78         Record details from a counter-proposal, updating the stored object with    79         attendance information.    80         """    81     82         # Obtain valid organiser and attendee details.    83     84         oa = self.require_organiser_and_attendees(from_organiser=False)    85         if not oa:    86             return False    87     88         (organiser, organiser_attr), attendees = oa    89     90         # The responding attendee is regarded as not attending.    91     92         for attendee, attendee_attr in attendees.items():    93             attendee_attr["PARTSTAT"] = "DECLINED"    94     95         # Update this attendance.    96     97         if self.merge_attendance(attendees):    98             self.update_freebusy_from_attendees(attendees)    99    100         # Queue any counter-proposal for perusal.   101    102         for attendee in attendees.keys():   103             self.store.set_counter(self.user, attendee, self.obj.to_node(), self.uid, self.recurrenceid)   104         self.store.queue_request(self.user, self.uid, self.recurrenceid, "COUNTER")   105    106         return True   107    108     def _record(self, from_organiser=True, queue=False, cancel=False):   109    110         """   111         Record details from the current object given a message originating   112         from an organiser if 'from_organiser' is set to a true value, queuing a   113         request if 'queue' is set to a true value, or cancelling an event if   114         'cancel' is set to a true value.   115         """   116    117         # Obtain valid organiser and attendee details.   118    119         oa = self.require_organiser_and_attendees(from_organiser)   120         if not oa:   121             return False   122    123         (organiser, organiser_attr), attendees = oa   124    125         # Handle notifications and invitations.   126    127         if from_organiser:   128    129             # Process for the current user, an attendee.   130    131             if not self.have_new_object():   132                 return False   133    134             # Remove additional recurrences if handling a complete event.   135    136             if not self.recurrenceid:   137                 self.store.remove_recurrences(self.user, self.uid)   138    139             # Queue any request, if appropriate.   140    141             if queue:   142                 self.store.queue_request(self.user, self.uid, self.recurrenceid)   143    144             # Cancel complete events or particular occurrences in recurring   145             # events.   146    147             if cancel:   148                 self.store.cancel_event(self.user, self.uid, self.recurrenceid)   149                 self.store.dequeue_request(self.user, self.uid, self.recurrenceid)   150    151                 # Remove any associated request.   152    153                 self.store.dequeue_request(self.user, self.uid, self.recurrenceid)   154    155                 # No return message will occur to update the free/busy   156                 # information, so this is done here using outgoing message   157                 # functionality.   158    159                 self.remove_event_from_freebusy()   160    161                 # Update the recipient's record of the organiser's schedule.   162    163                 self.remove_freebusy_from_organiser(organiser)   164    165             else:   166                 self.update_freebusy_from_organiser(organiser)   167    168             # Set the complete event or an additional occurrence.   169    170             self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())   171    172         # As organiser, update attendance from valid attendees.   173    174         else:   175             if self.merge_attendance(attendees):   176                 self.update_freebusy_from_attendees(attendees)   177    178         return True   179    180     def _refresh(self):   181    182         """   183         Respond to a refresh message by providing complete event details to   184         attendees.   185         """   186    187         # Obtain valid organiser and attendee details.   188    189         oa = self.require_organiser_and_attendees(False)   190         if not oa:   191             return False   192    193         (organiser, organiser_attr), attendees = oa   194    195         # Filter by stored attendees.   196    197         obj = self.get_stored_object_version()   198         stored_attendees = set(obj.get_values("ATTENDEE"))   199         attendees = stored_attendees.intersection(attendees)   200    201         if not attendees:   202             return False   203    204         # Assume that the outcome will be a request. It would not seem   205         # completely bizarre to produce a publishing message instead if a   206         # refresh message was unprovoked.   207    208         method = "REQUEST"   209    210         for attendee in attendees:   211             responses = []   212    213             # Get the parent event, add SENT-BY details to the organiser.   214    215             obj = self.get_stored_object_version()   216    217             if self.is_participating(attendee, obj=obj):   218                 organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))   219                 self.update_sender(organiser_attr)   220                 responses.append(obj.to_node())   221    222             # Get recurrences.   223    224             if not self.recurrenceid:   225                 for recurrenceid in self.store.get_active_recurrences(self.user, self.uid):   226    227                     # Get the recurrence, add SENT-BY details to the organiser.   228    229                     obj = self.get_stored_object(self.uid, recurrenceid)   230    231                     if self.is_participating(attendee, obj=obj):   232                         organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))   233                         self.update_sender(organiser_attr)   234                         responses.append(obj.to_node())   235    236             self.add_result(method, [get_address(attendee)], to_part(method, responses))   237    238         return True   239    240 class Event(PersonHandler):   241    242     "An event handler."   243    244     def add(self):   245    246         "Queue a suggested additional recurrence for any active event."   247    248         if self.allow_add() and self._add(queue=True):   249             return self.wrap("An addition to an event has been received.")   250    251     def cancel(self):   252    253         "Queue a cancellation of any active event."   254    255         if self._record(from_organiser=True, queue=False, cancel=True):   256             return self.wrap("An event cancellation has been received.", link=False)   257    258     def counter(self):   259    260         "Record a counter-proposal to a proposed event."   261    262         # NOTE: Queue a suggested modification to any active event.   263    264         if self._counter():   265             return self.wrap("A counter proposal to an event invitation has been received.", link=False)   266    267     def declinecounter(self):   268    269         # NOTE: Queue a rejected modification to any active event.   270    271         return self.wrap("Your counter proposal to an event invitation has been declined.", link=False)   272    273     def publish(self):   274    275         "Register details of any relevant event."   276    277         if self._record(from_organiser=True, queue=False):   278             return self.wrap("Details of an event have been received.")   279    280     def refresh(self):   281    282         "Requests to refresh events are handled either here or by the client."   283    284         if self.is_refreshing():   285             return self._refresh()   286         else:   287             return self.wrap("A request for updated event details has been received.")   288    289     def reply(self):   290    291         "Record replies and notify the recipient."   292    293         if self._record(from_organiser=False, queue=False):   294             return self.wrap("A reply to an event invitation has been received.")   295    296     def request(self):   297    298         "Hold requests and notify the recipient."   299    300         if self._record(from_organiser=True, queue=True):   301             return self.wrap("An event invitation has been received.")   302    303 class PersonFreebusy(CommonFreebusy, Handler):   304    305     "Free/busy handling mechanisms specific to people."   306    307     def _record_freebusy(self, from_organiser=True):   308    309         """   310         Record free/busy information for a message originating from an organiser   311         if 'from_organiser' is set to a true value.   312         """   313    314         if from_organiser:   315             organiser_item = self.require_organiser(from_organiser)   316             if not organiser_item:   317                 return   318    319             senders = [organiser_item]   320         else:   321             oa = self.require_organiser_and_attendees(from_organiser)   322             if not oa:   323                 return   324    325             organiser_item, attendees = oa   326             senders = attendees.items()   327    328             if not senders:   329                 return   330    331         freebusy = [FreeBusyPeriod(p.get_start_point(), p.get_end_point()) for p in self.obj.get_period_values("FREEBUSY")]   332         dtstart = self.obj.get_datetime("DTSTART")   333         dtend = self.obj.get_datetime("DTEND")   334         period = Period(dtstart, dtend, self.get_tzid())   335    336         for sender, sender_attr in senders:   337             stored_freebusy = self.store.get_freebusy_for_other(self.user, sender)   338             replace_overlapping(stored_freebusy, period, freebusy)   339             self.store.set_freebusy_for_other(self.user, stored_freebusy, sender)   340    341 class Freebusy(PersonFreebusy):   342    343     "A free/busy handler."   344    345     def publish(self):   346    347         "Register free/busy information."   348    349         self._record_freebusy(from_organiser=True)   350    351         # Produce a message if configured to do so.   352    353         if self.is_notifying():   354             return self.wrap("A free/busy update has been received.", link=False)   355    356     def reply(self):   357    358         "Record replies and notify the recipient."   359    360         self._record_freebusy(from_organiser=False)   361    362         # Produce a message if configured to do so.   363    364         if self.is_notifying():   365             return self.wrap("A reply to a free/busy request has been received.", link=False)   366    367     def request(self):   368    369         """   370         Respond to a request by preparing a reply containing free/busy   371         information for the recipient.   372         """   373    374         # Produce a reply if configured to do so.   375    376         if self.is_sharing():   377             return CommonFreebusy.request(self)   378    379 # Handler registry.   380    381 handlers = [   382     ("VFREEBUSY",   Freebusy),   383     ("VEVENT",      Event),   384     ]   385    386 # vim: tabstop=4 expandtab shiftwidth=4