imip-agent

imiptools/handlers/person.py

886:8a3994e54ea4
2015-10-20 Paul Boddie Permit the selection of a same-day ending while still allowing time adjustments.
     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    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             return    51     52         # Record the event as a recurrence of the parent object.    53     54         self.update_recurrenceid()    55     56         # Update the recipient's record of the organiser's schedule.    57     58         self.update_freebusy_from_organiser(organiser)    59     60         # Set the additional occurrence.    61     62         self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())    63     64         # Remove any previous cancellations involving this event.    65     66         self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)    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         # Update the attendance for the sender.    91     92         attendee = self.get_sending_attendee()    93         if not attendee:    94             return False    95     96         self.merge_attendance({attendee : attendees[attendee]})    97     98         # Queue any counter-proposal for perusal.    99    100         self.store.set_counter(self.user, attendee, self.obj.to_node(), self.uid, self.recurrenceid)   101         self.store.queue_request(self.user, self.uid, self.recurrenceid, "COUNTER")   102    103         return True   104    105     def _cancel(self):   106    107         "Record an event cancellation."   108    109         # Handle an event being published by the sender to themself.   110    111         organiser_item = self.require_organiser()   112         if organiser_item:   113             organiser, organiser_attr = organiser_item   114             if self.user == organiser:   115                 self.store.cancel_event(self.user, self.uid, self.recurrenceid)   116                 self.store.dequeue_request(self.user, self.uid, self.recurrenceid)   117                 self.store.remove_counters(self.user, self.uid, self.recurrenceid)   118                 self.remove_event_from_freebusy()   119                 return True   120    121         return self._record(from_organiser=True, queue=False, cancel=True)   122    123     def _declinecounter(self):   124    125         "Revoke any counter-proposal recorded as a free/busy offer."   126    127         # Obtain valid organiser and attendee details.   128    129         oa = self.require_organiser_and_attendees()   130         if not oa:   131             return False   132    133         self.remove_event_from_freebusy_offers()   134    135         return True   136    137     def _publish(self):   138    139         "Record details of a published event."   140    141         # Handle an event being published by the sender to themself.   142    143         organiser_item = self.require_organiser()   144         if organiser_item:   145             organiser, organiser_attr = organiser_item   146             if self.user == organiser:   147                 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())   148                 self.update_event_in_freebusy()   149                 return True   150    151         return self._record(from_organiser=True, queue=False)   152    153     def _record(self, from_organiser=True, queue=False, cancel=False):   154    155         """   156         Record details from the current object given a message originating   157         from an organiser if 'from_organiser' is set to a true value, queuing a   158         request if 'queue' is set to a true value, or cancelling an event if   159         'cancel' is set to a true value.   160         """   161    162         # Obtain valid organiser and attendee details.   163    164         oa = self.require_organiser_and_attendees(from_organiser)   165         if not oa:   166             return False   167    168         (organiser, organiser_attr), attendees = oa   169    170         # Handle notifications and invitations.   171    172         if from_organiser:   173    174             # Process for the current user, an attendee.   175    176             if not self.have_new_object():   177                 return False   178    179             # Remove additional recurrences if handling a complete event.   180             # Also remove any previous cancellations involving this event.   181    182             if not self.recurrenceid:   183                 self.store.remove_recurrences(self.user, self.uid)   184                 self.store.remove_cancellations(self.user, self.uid)   185             else:   186                 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)   187    188             # Queue any request, if appropriate.   189    190             if queue:   191                 self.store.queue_request(self.user, self.uid, self.recurrenceid)   192    193             # Set the complete event or an additional occurrence.   194    195             self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())   196    197             # Cancel complete events or particular occurrences in recurring   198             # events.   199    200             if cancel:   201                 self.store.cancel_event(self.user, self.uid, self.recurrenceid)   202    203                 # Remove any associated request.   204    205                 self.store.dequeue_request(self.user, self.uid, self.recurrenceid)   206                 self.store.remove_counters(self.user, self.uid, self.recurrenceid)   207    208                 # No return message will occur to update the free/busy   209                 # information, so this is done here using outgoing message   210                 # functionality.   211    212                 self.remove_event_from_freebusy()   213    214                 # Update the recipient's record of the organiser's schedule.   215    216                 self.remove_freebusy_from_organiser(organiser)   217    218             else:   219                 self.update_freebusy_from_organiser(organiser)   220    221         # As organiser, update attendance from valid attendees.   222    223         else:   224             if self.merge_attendance(attendees):   225                 self.update_freebusy_from_attendees(attendees)   226    227         return True   228    229     def _refresh(self):   230    231         """   232         Respond to a refresh message by providing complete event details to   233         attendees.   234         """   235    236         # Obtain valid organiser and attendee details.   237    238         oa = self.require_organiser_and_attendees(False)   239         if not oa:   240             return False   241    242         (organiser, organiser_attr), attendees = oa   243    244         # Filter by stored attendees.   245    246         obj = self.get_stored_object_version()   247         stored_attendees = set(obj.get_values("ATTENDEE"))   248         attendees = stored_attendees.intersection(attendees)   249    250         if not attendees:   251             return False   252    253         # Produce REQUEST and CANCEL results.   254    255         for attendee in attendees:   256             methods, parts = self.get_message_parts(obj, "REQUEST", attendee)   257             self.add_results(methods, [get_address(attendee)], parts)   258    259         return True   260    261 class Event(PersonHandler):   262    263     "An event handler."   264    265     def add(self):   266    267         "Queue a suggested additional recurrence for any active event."   268    269         if self.allow_add() and self._add(queue=True):   270             return self.wrap("An addition to an event has been received.")   271    272     def cancel(self):   273    274         "Queue a cancellation of any active event."   275    276         if self._cancel():   277             return self.wrap("An event cancellation has been received.", link=False)   278    279     def counter(self):   280    281         "Record a counter-proposal to a proposed event."   282    283         if self._counter():   284             return self.wrap("A counter proposal to an event invitation has been received.", link=True)   285    286     def declinecounter(self):   287    288         "Record a rejection of a counter-proposal."   289    290         if self._declinecounter():   291             return self.wrap("Your counter proposal to an event invitation has been declined.", link=True)   292    293     def publish(self):   294    295         "Register details of any relevant event."   296    297         if self._publish():   298             return self.wrap("Details of an event have been received.")   299    300     def refresh(self):   301    302         "Requests to refresh events are handled either here or by the client."   303    304         if self.is_refreshing():   305             return self._refresh()   306         else:   307             return self.wrap("A request for updated event details has been received.")   308    309     def reply(self):   310    311         "Record replies and notify the recipient."   312    313         if self._record(from_organiser=False, queue=False):   314             return self.wrap("A reply to an event invitation has been received.")   315    316     def request(self):   317    318         "Hold requests and notify the recipient."   319    320         if self._record(from_organiser=True, queue=True):   321             return self.wrap("An event invitation has been received.")   322    323 class PersonFreebusy(CommonFreebusy, Handler):   324    325     "Free/busy handling mechanisms specific to people."   326    327     def _record_freebusy(self, from_organiser=True):   328    329         """   330         Record free/busy information for a message originating from an organiser   331         if 'from_organiser' is set to a true value.   332         """   333    334         if from_organiser:   335             organiser_item = self.require_organiser(from_organiser)   336             if not organiser_item:   337                 return   338    339             senders = [organiser_item]   340         else:   341             oa = self.require_organiser_and_attendees(from_organiser)   342             if not oa:   343                 return   344    345             organiser_item, attendees = oa   346             senders = attendees.items()   347    348             if not senders:   349                 return   350    351         freebusy = [FreeBusyPeriod(p.get_start_point(), p.get_end_point()) for p in self.obj.get_period_values("FREEBUSY")]   352         dtstart = self.obj.get_datetime("DTSTART")   353         dtend = self.obj.get_datetime("DTEND")   354         period = Period(dtstart, dtend, self.get_tzid())   355    356         for sender, sender_attr in senders:   357             stored_freebusy = self.store.get_freebusy_for_other(self.user, sender)   358             replace_overlapping(stored_freebusy, period, freebusy)   359             self.store.set_freebusy_for_other(self.user, stored_freebusy, sender)   360    361 class Freebusy(PersonFreebusy):   362    363     "A free/busy handler."   364    365     def publish(self):   366    367         "Register free/busy information."   368    369         self._record_freebusy(from_organiser=True)   370    371         # Produce a message if configured to do so.   372    373         if self.is_notifying():   374             return self.wrap("A free/busy update has been received.", link=False)   375    376     def reply(self):   377    378         "Record replies and notify the recipient."   379    380         self._record_freebusy(from_organiser=False)   381    382         # Produce a message if configured to do so.   383    384         if self.is_notifying():   385             return self.wrap("A reply to a free/busy request has been received.", link=False)   386    387     def request(self):   388    389         """   390         Respond to a request by preparing a reply containing free/busy   391         information for the recipient.   392         """   393    394         # Produce a reply if configured to do so.   395    396         if self.is_sharing():   397             return CommonFreebusy.request(self)   398    399 # Handler registry.   400    401 handlers = [   402     ("VFREEBUSY",   Freebusy),   403     ("VEVENT",      Event),   404     ]   405    406 # vim: tabstop=4 expandtab shiftwidth=4