imip-agent

imiptools/client.py

1075:9c9c0a802ad1
2016-03-07 Paul Boddie Moved free/busy provider methods into the store base class.
     1 #!/usr/bin/env python     2      3 """     4 Common calendar client utilities.     5      6 Copyright (C) 2014, 2015, 2016 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 datetime import datetime, timedelta    23 from imiptools import config    24 from imiptools.data import Object, get_address, get_uri, get_window_end, \    25                            is_new_object, make_freebusy, to_part, \    26                            uri_dict, uri_item, uri_items, uri_parts, uri_values    27 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \    28                             get_duration, get_timestamp    29 from imiptools.i18n import get_translator    30 from imiptools.period import can_schedule, remove_event_periods, \    31                              remove_additional_periods, remove_affected_period, \    32                              update_freebusy    33 from imiptools.profile import Preferences    34 import imiptools.stores.file    35     36 class Client:    37     38     "Common handler and manager methods."    39     40     default_window_size = 100    41     organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST"    42     43     def __init__(self, user, messenger=None, store=None, publisher=None, journal=None,    44                  preferences_dir=None):    45     46         """    47         Initialise a calendar client with the current 'user', plus any    48         'messenger', 'store', 'publisher' and 'journal' objects, indicating any    49         specific 'preferences_dir'.    50         """    51     52         self.user = user    53         self.messenger = messenger    54         self.store = store or imiptools.stores.file.FileStore()    55         self.journal = journal or imiptools.stores.file.FileJournal()    56     57         try:    58             self.publisher = publisher or imiptools.stores.file.FilePublisher()    59         except OSError:    60             self.publisher = None    61     62         self.preferences_dir = preferences_dir    63         self.preferences = None    64     65         # Localise the messenger.    66     67         if self.messenger:    68             self.messenger.gettext = self.get_translator()    69     70     def get_store(self):    71         return self.store    72     73     def get_publisher(self):    74         return self.publisher    75     76     def get_journal(self):    77         return self.journal    78     79     # Store-related methods.    80     81     def acquire_lock(self):    82         self.store.acquire_lock(self.user)    83     84     def release_lock(self):    85         self.store.release_lock(self.user)    86     87     # Preferences-related methods.    88     89     def get_preferences(self):    90         if not self.preferences and self.user:    91             self.preferences = Preferences(self.user, self.preferences_dir)    92         return self.preferences    93     94     def get_locale(self):    95         prefs = self.get_preferences()    96         return prefs and prefs.get("LANG", "en", True) or "en"    97     98     def get_translator(self):    99         return get_translator([self.get_locale()])   100    101     def get_user_attributes(self):   102         prefs = self.get_preferences()   103         return prefs and prefs.get_all(["CN"]) or {}   104    105     def get_tzid(self):   106         prefs = self.get_preferences()   107         return prefs and prefs.get("TZID") or get_default_timezone()   108    109     def get_window_size(self):   110         prefs = self.get_preferences()   111         try:   112             return prefs and int(prefs.get("window_size")) or self.default_window_size   113         except (TypeError, ValueError):   114             return self.default_window_size   115    116     def get_window_end(self):   117         return get_window_end(self.get_tzid(), self.get_window_size())   118    119     def is_participating(self):   120    121         "Return participation in the calendar system."   122    123         prefs = self.get_preferences()   124         return prefs and prefs.get("participating", config.PARTICIPATING_DEFAULT) != "no" or False   125    126     def is_sharing(self):   127    128         "Return whether free/busy information is being generally shared."   129    130         prefs = self.get_preferences()   131         return prefs and prefs.get("freebusy_sharing", config.SHARING_DEFAULT) == "share" or False   132    133     def is_bundling(self):   134    135         "Return whether free/busy information is being bundled in messages."   136    137         prefs = self.get_preferences()   138         return prefs and prefs.get("freebusy_bundling", config.BUNDLING_DEFAULT) == "always" or False   139    140     def is_notifying(self):   141    142         "Return whether recipients are notified about free/busy payloads."   143    144         prefs = self.get_preferences()   145         return prefs and prefs.get("freebusy_messages", config.NOTIFYING_DEFAULT) == "notify" or False   146    147     def is_publishing(self):   148    149         "Return whether free/busy information is being published as Web resources."   150    151         prefs = self.get_preferences()   152         return prefs and prefs.get("freebusy_publishing", config.PUBLISHING_DEFAULT) == "publish" or False   153    154     def is_refreshing(self):   155    156         "Return whether a recipient supports requests to refresh event details."   157    158         prefs = self.get_preferences()   159         return prefs and prefs.get("event_refreshing", config.REFRESHING_DEFAULT) == "always" or False   160    161     def allow_add(self):   162         return self.get_add_method_response() in ("add", "refresh")   163    164     def get_add_method_response(self):   165         prefs = self.get_preferences()   166         return prefs and prefs.get("add_method_response", config.ADD_RESPONSE_DEFAULT) or "refresh"   167    168     def get_offer_period(self):   169    170         "Decode a specification in the iCalendar duration format."   171    172         prefs = self.get_preferences()   173         duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT)   174    175         # NOTE: Should probably report an error somehow if None.   176    177         return duration and get_duration(duration) or None   178    179     def get_organiser_replacement(self):   180         prefs = self.get_preferences()   181         return prefs and prefs.get("organiser_replacement", config.ORGANISER_REPLACEMENT_DEFAULT) or "attendee"   182    183     def have_manager(self):   184         return config.MANAGER_INTERFACE   185    186     def get_permitted_values(self):   187    188         """   189         Decode a specification of one of the following forms...   190    191         <minute values>   192         <hour values>:<minute values>   193         <hour values>:<minute values>:<second values>   194    195         ...with each list of values being comma-separated.   196         """   197    198         prefs = self.get_preferences()   199         permitted_values = prefs and prefs.get("permitted_times")   200         if permitted_values:   201             try:   202                 l = []   203                 for component in permitted_values.split(":")[:3]:   204                     if component:   205                         l.append(map(int, component.split(",")))   206                     else:   207                         l.append(None)   208    209             # NOTE: Should probably report an error somehow.   210    211             except ValueError:   212                 return None   213             else:   214                 l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or [])   215                 return l   216         else:   217             return None   218    219     # Common operations on calendar data.   220    221     def update_sender(self, attr):   222    223         "Update the SENT-BY attribute of the 'attr' sender metadata."   224    225         if self.messenger and self.messenger.sender != get_address(self.user):   226             attr["SENT-BY"] = get_uri(self.messenger.sender)   227    228     def get_periods(self, obj, explicit_only=False):   229    230         """   231         Return periods for the given 'obj'. Interpretation of periods can depend   232         on the time zone, which is obtained for the current user. If   233         'explicit_only' is set to a true value, only explicit periods will be   234         returned, not rule-based periods.   235         """   236    237         return obj.get_periods(self.get_tzid(), not explicit_only and self.get_window_end() or None)   238    239     # Store operations.   240    241     def get_stored_object(self, uid, recurrenceid, section=None, username=None):   242    243         """   244         Return the stored object for the current user, with the given 'uid' and   245         'recurrenceid' from the given 'section' and for the given 'username' (if   246         specified), or from the standard object collection otherwise.   247         """   248    249         if section == "counters":   250             fragment = self.store.get_counter(self.user, username, uid, recurrenceid)   251         else:   252             fragment = self.store.get_event(self.user, uid, recurrenceid, section)   253         return fragment and Object(fragment)   254    255     # Free/busy operations.   256    257     def get_freebusy_part(self, freebusy=None):   258    259         """   260         Return a message part containing free/busy information for the user,   261         either specified as 'freebusy' or obtained from the store directly.   262         """   263    264         if self.is_sharing() and self.is_bundling():   265    266             # Invent a unique identifier.   267    268             utcnow = get_timestamp()   269             uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))   270    271             freebusy = freebusy or self.store.get_freebusy(self.user)   272    273             user_attr = {}   274             self.update_sender(user_attr)   275             return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])   276    277         return None   278    279     def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):   280    281         """   282         Update the 'freebusy' collection with the given 'periods', indicating a   283         'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a   284         recurrence or the parent event. The 'summary' and 'organiser' must also   285         be provided.   286    287         An optional 'expires' datetime string can be provided to tag a free/busy   288         offer.   289         """   290    291         update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)   292    293     # Preparation of messages communicating the state of events.   294    295     def get_message_parts(self, obj, method, attendee=None):   296    297         """   298         Return a tuple containing a list of methods and a list of message parts,   299         with the parts collectively describing the given object 'obj' and its   300         recurrences, using 'method' as the means of publishing details (with   301         CANCEL being used to retract or remove details).   302    303         If 'attendee' is indicated, the attendee's participation will be taken   304         into account when generating the description.   305         """   306    307         # Assume that the outcome will be composed of requests and   308         # cancellations. It would not seem completely bizarre to produce   309         # publishing messages if a refresh message was unprovoked.   310    311         responses = []   312         methods = set()   313    314         # Get the parent event, add SENT-BY details to the organiser.   315    316         if not attendee or self.is_participating(attendee, obj=obj):   317             organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))   318             self.update_sender(organiser_attr)   319             responses.append(obj.to_part(method))   320             methods.add(method)   321    322         # Get recurrences for parent events.   323    324         if not self.recurrenceid:   325    326             # Collect active and cancelled recurrences.   327    328             for rl, section, rmethod in [   329                 (self.store.get_active_recurrences(self.user, self.uid), None, method),   330                 (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"),   331                 ]:   332    333                 for recurrenceid in rl:   334    335                     # Get the recurrence, add SENT-BY details to the organiser.   336    337                     obj = self.get_stored_object(self.uid, recurrenceid, section)   338    339                     if not attendee or self.is_participating(attendee, obj=obj):   340                         organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))   341                         self.update_sender(organiser_attr)   342                         responses.append(obj.to_part(rmethod))   343                         methods.add(rmethod)   344    345         return methods, responses   346    347 class ClientForObject(Client):   348    349     "A client maintaining a specific object."   350    351     def __init__(self, obj, user, messenger=None, store=None, publisher=None,   352                  journal=None, preferences_dir=None):   353         Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir)   354         self.set_object(obj)   355    356     def set_object(self, obj):   357    358         "Set the current object to 'obj', obtaining metadata details."   359    360         self.obj = obj   361         self.uid = obj and self.obj.get_uid()   362         self.recurrenceid = obj and self.obj.get_recurrenceid()   363         self.sequence = obj and self.obj.get_value("SEQUENCE")   364         self.dtstamp = obj and self.obj.get_value("DTSTAMP")   365    366     def set_identity(self, method):   367    368         """   369         Set the current user for the current object in the context of the given   370         'method'. It is usually set when initialising the handler, using the   371         recipient details, but outgoing messages do not reference the recipient   372         in this way.   373         """   374    375         pass   376    377     def is_usable(self, method=None):   378    379         "Return whether the current object is usable with the given 'method'."   380    381         return True   382    383     def is_organiser(self):   384    385         """   386         Return whether the current user is the organiser in the current object.   387         """   388    389         return get_uri(self.obj.get_value("ORGANIZER")) == self.user   390    391     # Common operations on calendar data.   392    393     def update_senders(self, obj=None):   394    395         """   396         Update sender details in 'obj', or the current object if not indicated,   397         removing SENT-BY attributes for attendees other than the current user if   398         those attributes give the URI of the calendar system.   399         """   400    401         obj = obj or self.obj   402         calendar_uri = self.messenger and get_uri(self.messenger.sender)   403         for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")):   404             if attendee != self.user:   405                 if attendee_attr.get("SENT-BY") == calendar_uri:   406                     del attendee_attr["SENT-BY"]   407             else:   408                 attendee_attr["SENT-BY"] = calendar_uri   409    410     def get_sending_attendee(self):   411    412         "Return the attendee who sent the current object."   413    414         # Search for the sender of the message or the calendar system address.   415    416         senders = self.senders or self.messenger and [self.messenger.sender] or []   417    418         for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):   419             if get_address(attendee) in senders or \   420                get_address(attendee_attr.get("SENT-BY")) in senders:   421                 return get_uri(attendee)   422    423         return None   424    425     def get_unscheduled_parts(self, periods):   426    427         "Return message parts describing unscheduled 'periods'."   428    429         unscheduled_parts = []   430    431         if periods:   432             obj = self.obj.copy()   433             obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])   434    435             for p in periods:   436                 if not p.origin:   437                     continue   438                 obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())]   439                 obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())]   440                 unscheduled_parts.append(obj.to_part("CANCEL"))   441    442         return unscheduled_parts   443    444     # Object update methods.   445    446     def update_recurrenceid(self):   447    448         """   449         Update the RECURRENCE-ID in the current object, initialising it from   450         DTSTART.   451         """   452    453         self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")]   454         self.recurrenceid = self.obj.get_recurrenceid()   455    456     def update_dtstamp(self, obj=None):   457    458         "Update the DTSTAMP in the current object or any given object 'obj'."   459    460         obj = obj or self.obj   461         self.dtstamp = obj.update_dtstamp()   462    463     def update_sequence(self, increment=False, obj=None):   464    465         "Update the SEQUENCE in the current object or any given object 'obj'."   466    467         obj = obj or self.obj   468         obj.update_sequence(increment)   469    470     def merge_attendance(self, attendees):   471    472         """   473         Merge attendance from the current object's 'attendees' into the version   474         stored for the current user.   475         """   476    477         obj = self.get_stored_object_version()   478    479         if not obj or not self.have_new_object():   480             return False   481    482         # Get attendee details in a usable form.   483    484         attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))   485    486         for attendee, attendee_attr in attendees.items():   487    488             # Update attendance in the loaded object for any recognised   489             # attendees.   490    491             if attendee_map.has_key(attendee):   492                 attendee_map[attendee] = attendee_attr   493    494         # Set the new details and store the object.   495    496         obj["ATTENDEE"] = attendee_map.items()   497    498         # Set a specific recurrence or the complete event if not an additional   499         # occurrence.   500    501         return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node())   502    503     def update_attendees(self, attendees, removed):   504    505         """   506         Update the attendees in the current object with the given 'attendees'   507         and 'removed' attendee lists.   508    509         A tuple is returned containing two items: a list of the attendees whose   510         attendance is being proposed (in a counter-proposal), a list of the   511         attendees whose attendance should be cancelled.   512         """   513    514         to_cancel = []   515    516         existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or [])   517         existing_attendees_map = dict(existing_attendees)   518    519         # Added attendees are those from the supplied collection not already   520         # present in the object.   521    522         added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees])   523         removed = uri_values(removed)   524    525         if added or removed:   526    527             # The organiser can remove existing attendees.   528    529             if removed and self.is_organiser():   530                 remaining = []   531    532                 for attendee, attendee_attr in existing_attendees:   533                     if attendee in removed:   534    535                         # Only when an event has not been published can   536                         # attendees be silently removed.   537    538                         if self.obj.is_shared():   539                             to_cancel.append((attendee, attendee_attr))   540                     else:   541                         remaining.append((attendee, attendee_attr))   542    543                 existing_attendees = remaining   544    545             # Attendees (when countering) must only include the current user and   546             # any added attendees.   547    548             elif not self.is_organiser():   549                 existing_attendees = []   550    551             # Both organisers and attendees (when countering) can add attendees.   552    553             if added:   554    555                 # Obtain a mapping from URIs to name details.   556    557                 attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)])   558    559                 for attendee in added:   560                     attendee = attendee.strip()   561                     if attendee:   562                         cn = attendee_map.get(attendee)   563                         attendee_attr = {"CN" : cn} or {}   564    565                         # Only the organiser can reset the participation attributes.   566    567                         if self.is_organiser():   568                             attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})   569    570                         existing_attendees.append((attendee, attendee_attr))   571    572             # Attendees (when countering) must only include the current user and   573             # any added attendees.   574    575             if not self.is_organiser() and self.user not in existing_attendees:   576                 user_attr = self.get_user_attributes()   577                 user_attr.update(existing_attendees_map.get(self.user) or {})   578                 existing_attendees.append((self.user, user_attr))   579    580             self.obj["ATTENDEE"] = existing_attendees   581    582         return added, to_cancel   583    584     def update_participation(self, partstat=None):   585    586         """   587         Update the participation in the current object of the user with the   588         given 'partstat'.   589         """   590    591         attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user)   592         if not attendee_attr:   593             return None   594         if partstat:   595             attendee_attr["PARTSTAT"] = partstat   596         if attendee_attr.has_key("RSVP"):   597             del attendee_attr["RSVP"]   598         self.update_sender(attendee_attr)   599         return attendee_attr   600    601     # Communication methods.   602    603     def send_message(self, parts, sender, obj, from_organiser, bcc_sender):   604    605         """   606         Send the given 'parts' to the appropriate recipients, also sending a   607         copy to the 'sender'. The 'obj' together with the 'from_organiser' value   608         (which indicates whether the organiser is sending this message) are used   609         to determine the recipients of the message.   610         """   611    612         # As organiser, send an invitation to attendees, excluding oneself if   613         # also attending. The updated event will be saved by the outgoing   614         # handler.   615    616         organiser = get_uri(obj.get_value("ORGANIZER"))   617         attendees = uri_values(obj.get_values("ATTENDEE"))   618    619         if from_organiser:   620             recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]   621         else:   622             recipients = [get_address(organiser)]   623    624         # Since the outgoing handler updates this user's free/busy details,   625         # the stored details will probably not have the updated details at   626         # this point, so we update our copy for serialisation as the bundled   627         # free/busy object.   628    629         freebusy = self.store.get_freebusy(self.user)   630         self.update_freebusy(freebusy, self.user, from_organiser)   631    632         # Bundle free/busy information if appropriate.   633    634         part = self.get_freebusy_part(freebusy)   635         if part:   636             parts.append(part)   637    638         if recipients or bcc_sender:   639             self._send_message(sender, recipients, parts, bcc_sender)   640    641     def _send_message(self, sender, recipients, parts, bcc_sender):   642    643         """   644         Send a message, explicitly specifying the 'sender' as an outgoing BCC   645         recipient since the generic calendar user will be the actual sender.   646         """   647    648         if not self.messenger:   649             return   650    651         if not bcc_sender:   652             message = self.messenger.make_outgoing_message(parts, recipients)   653             self.messenger.sendmail(recipients, message.as_string())   654         else:   655             message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)   656             self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)   657    658     def send_message_to_self(self, parts):   659    660         "Send a message composed of the given 'parts' to the given user."   661    662         if not self.messenger:   663             return   664    665         sender = get_address(self.user)   666         message = self.messenger.make_outgoing_message(parts, [sender])   667         self.messenger.sendmail([sender], message.as_string())   668    669     # Action methods.   670    671     def process_declined_counter(self, attendee):   672    673         "Process a declined counter-proposal."   674    675         # Obtain the counter-proposal for the attendee.   676    677         obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)   678         if not obj:   679             return False   680    681         method = "DECLINECOUNTER"   682         self.update_senders(obj=obj)   683         obj.update_dtstamp()   684         obj.update_sequence(False)   685         self._send_message(get_address(self.user), [get_address(attendee)], [obj.to_part(method)], True)   686         return True   687    688     def process_received_request(self, changed=False):   689    690         """   691         Process the current request for the current user. Return whether any   692         action was taken. If 'changed' is set to a true value, or if 'attendees'   693         is specified and differs from the stored attendees, a counter-proposal   694         will be sent instead of a reply.   695         """   696    697         # Reply only on behalf of this user.   698    699         attendee_attr = self.update_participation()   700    701         if not attendee_attr:   702             return False   703    704         if not changed:   705             self.obj["ATTENDEE"] = [(self.user, attendee_attr)]   706         else:   707             self.update_senders()   708    709         self.update_dtstamp()   710         self.update_sequence(False)   711         self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), self.obj, False, True)   712         return True   713    714     def process_created_request(self, method, to_cancel=None, to_unschedule=None):   715    716         """   717         Process the current request, sending a created request of the given   718         'method' to attendees. Return whether any action was taken.   719    720         If 'to_cancel' is specified, a list of participants to be sent cancel   721         messages is provided.   722    723         If 'to_unschedule' is specified, a list of periods to be unscheduled is   724         provided.   725         """   726    727         # Here, the organiser should be the current user.   728    729         organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))   730    731         self.update_sender(organiser_attr)   732         self.update_senders()   733         self.update_dtstamp()   734         self.update_sequence(True)   735    736         if method == "REQUEST":   737             methods, parts = self.get_message_parts(self.obj, "REQUEST")   738    739             # Add message parts with cancelled occurrence information.   740    741             unscheduled_parts = self.get_unscheduled_parts(to_unschedule)   742    743             # Send the updated event, along with a cancellation for each of the   744             # unscheduled occurrences.   745    746             self.send_message(parts + unscheduled_parts, get_address(organiser), self.obj, True, False)   747    748             # Since the organiser can update the SEQUENCE but this can leave any   749             # mail/calendar client lagging, issue a PUBLISH message to the   750             # user's address.   751    752             methods, parts = self.get_message_parts(self.obj, "PUBLISH")   753             self.send_message_to_self(parts + unscheduled_parts)   754    755         # When cancelling, replace the attendees with those for whom the event   756         # is now cancelled.   757    758         if method == "CANCEL" or to_cancel:   759             if to_cancel:   760                 obj = self.obj.copy()   761                 obj["ATTENDEE"] = to_cancel   762             else:   763                 obj = self.obj   764    765             # Send a cancellation to all uninvited attendees.   766    767             parts = [obj.to_part("CANCEL")]   768             self.send_message(parts, get_address(organiser), obj, True, False)   769    770             # Issue a CANCEL message to the user's address.   771    772             if method == "CANCEL":   773                 self.send_message_to_self(parts)   774    775         return True   776    777     # Object-related tests.   778    779     def is_recognised_organiser(self, organiser):   780    781         """   782         Return whether the given 'organiser' is recognised from   783         previously-received details. If no stored details exist, True is   784         returned.   785         """   786    787         obj = self.get_stored_object_version()   788         if obj:   789             stored_organiser = get_uri(obj.get_value("ORGANIZER"))   790             return stored_organiser == organiser   791         else:   792             return True   793    794     def is_recognised_attendee(self, attendee):   795    796         """   797         Return whether the given 'attendee' is recognised from   798         previously-received details. If no stored details exist, True is   799         returned.   800         """   801    802         obj = self.get_stored_object_version()   803         if obj:   804             stored_attendees = uri_dict(obj.get_value_map("ATTENDEE"))   805             return stored_attendees.has_key(attendee)   806         else:   807             return True   808    809     def get_attendance(self, user=None, obj=None):   810    811         """   812         Return the attendance attributes for 'user', or the current user if   813         'user' is not specified.   814         """   815    816         attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE"))   817         return attendees.get(user or self.user)   818    819     def is_participating(self, user, as_organiser=False, obj=None):   820    821         """   822         Return whether, subject to the 'user' indicating an identity and the   823         'as_organiser' status of that identity, the user concerned is actually   824         participating in the current object event.   825         """   826    827         # Use any attendee property information for an organiser, not the   828         # organiser property attributes.   829    830         attr = self.get_attendance(user, obj)   831         return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") not in ("DECLINED", "NEEDS-ACTION")   832    833     def has_indicated_attendance(self, user=None, obj=None):   834    835         """   836         Return whether the given 'user' (or the current user if not specified)   837         has indicated attendance in the given 'obj' (or the current object if   838         not specified).   839         """   840    841         attr = self.get_attendance(user, obj)   842         return attr and attr.get("PARTSTAT") not in (None, "NEEDS-ACTION")   843    844     def get_overriding_transparency(self, user, as_organiser=False):   845    846         """   847         Return the overriding transparency to be associated with the free/busy   848         records for an event, subject to the 'user' indicating an identity and   849         the 'as_organiser' status of that identity.   850    851         Where an identity is only an organiser and not attending, "ORG" is   852         returned. Otherwise, no overriding transparency is defined and None is   853         returned.   854         """   855    856         attr = self.get_attendance(user)   857         return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None   858    859     def can_schedule(self, freebusy, periods):   860    861         """   862         Indicate whether within 'freebusy' the given 'periods' can be scheduled.   863         """   864    865         return can_schedule(freebusy, periods, self.uid, self.recurrenceid)   866    867     def have_new_object(self, strict=True):   868    869         """   870         Return whether the current object is new to the current user.   871    872         If 'strict' is specified and is a false value, the DTSTAMP test will be   873         ignored. This is useful in handling responses from attendees from   874         clients (like Claws Mail) that erase time information from DTSTAMP and   875         make it invalid.   876         """   877    878         obj = self.get_stored_object_version()   879    880         # If found, compare SEQUENCE and potentially DTSTAMP.   881    882         if obj:   883             sequence = obj.get_value("SEQUENCE")   884             dtstamp = obj.get_value("DTSTAMP")   885    886             # If the request refers to an older version of the object, ignore   887             # it.   888    889             return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict)   890    891         return True   892    893     def possibly_recurring_indefinitely(self):   894    895         "Return whether the object recurs indefinitely."   896    897         # Obtain the stored object to make sure that recurrence information   898         # is not being ignored. This might happen if a client sends a   899         # cancellation without the complete set of properties, for instance.   900    901         return self.obj.possibly_recurring_indefinitely() or \   902                self.get_stored_object_version() and \   903                self.get_stored_object_version().possibly_recurring_indefinitely()   904    905     # Constraint application on event periods.   906    907     def check_object(self):   908    909         "Check the object against any scheduling constraints."   910    911         permitted_values = self.get_permitted_values()   912         if not permitted_values:   913             return None   914    915         invalid = []   916    917         for period in self.obj.get_periods(self.get_tzid()):   918             errors = period.check_permitted(permitted_values)   919             if errors:   920                 start_errors, end_errors = errors   921                 invalid.append((period.origin, start_errors, end_errors))   922    923         return invalid   924    925     def correct_object(self):   926    927         "Correct the object according to any scheduling constraints."   928    929         permitted_values = self.get_permitted_values()   930         return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values)   931    932     def correct_period(self, period):   933    934         "Correct 'period' according to any scheduling constraints."   935    936         permitted_values = self.get_permitted_values()   937         if not permitted_values:   938             return period   939         else:   940             return period.get_corrected(permitted_values)   941    942     # Object retrieval.   943    944     def get_stored_object_version(self):   945    946         """   947         Return the stored object to which the current object refers for the   948         current user.   949         """   950    951         return self.get_stored_object(self.uid, self.recurrenceid)   952    953     def get_definitive_object(self, as_organiser):   954    955         """   956         Return an object considered definitive for the current transaction,   957         using 'as_organiser' to select the current transaction's object if   958         false, or selecting a stored object if true.   959         """   960    961         return not as_organiser and self.obj or self.get_stored_object_version()   962    963     def get_parent_object(self):   964    965         """   966         Return the parent object to which the current object refers for the   967         current user.   968         """   969    970         return self.recurrenceid and self.get_stored_object(self.uid, None) or None   971    972     def revert_cancellations(self, periods):   973    974         """   975         Restore cancelled recurrences corresponding to any of the given   976         'periods'.   977         """   978    979         for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid):   980             obj = self.get_stored_object(self.uid, recurrenceid, "cancellations")   981             if set(self.get_periods(obj)).intersection(periods):   982                 self.store.remove_cancellation(self.user, self.uid, recurrenceid)   983    984     # Convenience methods for modifying free/busy collections.   985    986     def get_recurrence_start_point(self, recurrenceid):   987    988         "Get 'recurrenceid' in a form suitable for matching free/busy entries."   989    990         return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid())   991    992     def remove_from_freebusy(self, freebusy):   993    994         "Remove this event from the given 'freebusy' collection."   995    996         removed = remove_event_periods(freebusy, self.uid, self.recurrenceid)   997         if not removed and self.recurrenceid:   998             return remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))   999         else:  1000             return removed  1001   1002     def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):  1003   1004         """  1005         Remove from 'freebusy' any original recurrence from parent free/busy  1006         details for the current object, if the current object is a specific  1007         additional recurrence. Otherwise, remove all additional recurrence  1008         information corresponding to 'recurrenceids', or if omitted, all  1009         recurrences.  1010         """  1011   1012         if self.recurrenceid:  1013             recurrenceid = self.get_recurrence_start_point(self.recurrenceid)  1014             remove_affected_period(freebusy, self.uid, recurrenceid)  1015         else:  1016             # Remove obsolete recurrence periods.  1017   1018             remove_additional_periods(freebusy, self.uid, recurrenceids)  1019   1020             # Remove original periods affected by additional recurrences.  1021   1022             if recurrenceids:  1023                 for recurrenceid in recurrenceids:  1024                     recurrenceid = self.get_recurrence_start_point(recurrenceid)  1025                     remove_affected_period(freebusy, self.uid, recurrenceid)  1026   1027     def update_freebusy(self, freebusy, user, as_organiser, offer=False):  1028   1029         """  1030         Update the 'freebusy' collection for this event with the periods and  1031         transparency associated with the current object, subject to the 'user'  1032         identity and the attendance details provided for them, indicating  1033         whether the update is being done 'as_organiser' (for the organiser of  1034         an event) or not.  1035   1036         If 'offer' is set to a true value, any free/busy updates will be tagged  1037         with an expiry time.  1038         """  1039   1040         # Obtain the stored object if the current object is not issued by the  1041         # organiser. Attendees do not have the opportunity to redefine the  1042         # periods.  1043   1044         obj = self.get_definitive_object(as_organiser)  1045         if not obj:  1046             return  1047   1048         # Obtain the affected periods.  1049   1050         periods = self.get_periods(obj)  1051   1052         # Define an overriding transparency, the indicated event transparency,  1053         # or the default transparency for the free/busy entry.  1054   1055         transp = self.get_overriding_transparency(user, as_organiser) or \  1056                  obj.get_value("TRANSP") or \  1057                  "OPAQUE"  1058   1059         # Calculate any expiry time. If no offer period is defined, do not  1060         # record the offer periods.  1061   1062         if offer:  1063             offer_period = self.get_offer_period()  1064             if offer_period:  1065                 expires = get_timestamp(offer_period)  1066             else:  1067                 return  1068         else:  1069             expires = None  1070   1071         # Perform the low-level update.  1072   1073         Client.update_freebusy(self, freebusy, periods, transp,  1074             self.uid, self.recurrenceid,  1075             obj.get_value("SUMMARY"),  1076             get_uri(obj.get_value("ORGANIZER")),  1077             expires)  1078   1079     def update_freebusy_for_participant(self, freebusy, user, for_organiser=False,  1080                                         updating_other=False, offer=False):  1081   1082         """  1083         Update the 'freebusy' collection for the given 'user', indicating  1084         whether the update is 'for_organiser' (being done for the organiser of  1085         an event) or not, and whether it is 'updating_other' (meaning another  1086         user's details).  1087   1088         If 'offer' is set to a true value, any free/busy updates will be tagged  1089         with an expiry time.  1090         """  1091   1092         # Record in the free/busy details unless a non-participating attendee.  1093         # Remove periods for non-participating attendees.  1094   1095         if offer or self.is_participating(user, for_organiser and not updating_other):  1096             self.update_freebusy(freebusy, user,  1097                 for_organiser and not updating_other or  1098                 not for_organiser and updating_other,  1099                 offer  1100                 )  1101         else:  1102             self.remove_from_freebusy(freebusy)  1103   1104     def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False,  1105                                         updating_other=False):  1106   1107         """  1108         Remove details from the 'freebusy' collection for the given 'user',  1109         indicating whether the modification is 'for_organiser' (being done for  1110         the organiser of an event) or not, and whether it is 'updating_other'  1111         (meaning another user's details).  1112         """  1113   1114         # Remove from the free/busy details if a specified attendee.  1115   1116         if self.is_participating(user, for_organiser and not updating_other):  1117             self.remove_from_freebusy(freebusy)  1118   1119     # Convenience methods for updating stored free/busy information received  1120     # from other users.  1121   1122     def update_freebusy_from_participant(self, user, for_organiser, fn=None):  1123   1124         """  1125         For the current user, record the free/busy information for another  1126         'user', indicating whether the update is 'for_organiser' or not, thus  1127         maintaining a separate record of their free/busy details.  1128         """  1129   1130         fn = fn or self.update_freebusy_for_participant  1131   1132         # A user does not store free/busy information for themself as another  1133         # party.  1134   1135         if user == self.user:  1136             return  1137   1138         self.acquire_lock()  1139         try:  1140             freebusy = self.store.get_freebusy_for_other(self.user, user)  1141             fn(freebusy, user, for_organiser, True)  1142   1143             # Tidy up any obsolete recurrences.  1144   1145             self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))  1146             self.store.set_freebusy_for_other(self.user, freebusy, user)  1147   1148         finally:  1149             self.release_lock()  1150   1151     def update_freebusy_from_organiser(self, organiser):  1152   1153         "For the current user, record free/busy information from 'organiser'."  1154   1155         self.update_freebusy_from_participant(organiser, True)  1156   1157     def update_freebusy_from_attendees(self, attendees):  1158   1159         "For the current user, record free/busy information from 'attendees'."  1160   1161         obj = self.get_stored_object_version()  1162   1163         if not obj or not self.have_new_object():  1164             return  1165   1166         # Filter out unrecognised attendees.  1167   1168         attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE")))  1169   1170         for attendee in attendees:  1171             self.update_freebusy_from_participant(attendee, False)  1172   1173     def remove_freebusy_from_organiser(self, organiser):  1174   1175         "For the current user, remove free/busy information from 'organiser'."  1176   1177         self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant)  1178   1179     def remove_freebusy_from_attendees(self, attendees):  1180   1181         "For the current user, remove free/busy information from 'attendees'."  1182   1183         for attendee in attendees.keys():  1184             self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant)  1185   1186     # Convenience methods for updating free/busy details at the event level.  1187   1188     def update_event_in_freebusy(self, for_organiser=True):  1189   1190         """  1191         Update free/busy information when handling an object, doing so for the  1192         organiser of an event if 'for_organiser' is set to a true value.  1193         """  1194   1195         freebusy = self.store.get_freebusy(self.user)  1196   1197         # Obtain the attendance attributes for this user, if available.  1198   1199         self.update_freebusy_for_participant(freebusy, self.user, for_organiser)  1200   1201         # Remove original recurrence details replaced by additional  1202         # recurrences, as well as obsolete additional recurrences.  1203   1204         self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))  1205         self.store.set_freebusy(self.user, freebusy)  1206   1207         if self.publisher and self.is_sharing() and self.is_publishing():  1208             self.publisher.set_freebusy(self.user, freebusy)  1209   1210         # Update free/busy provider information if the event may recur  1211         # indefinitely.  1212   1213         if self.possibly_recurring_indefinitely():  1214             self.store.append_freebusy_provider(self.user, self.obj)  1215   1216         return True  1217   1218     def remove_event_from_freebusy(self):  1219   1220         "Remove free/busy information when handling an object."  1221   1222         freebusy = self.store.get_freebusy(self.user)  1223   1224         self.remove_from_freebusy(freebusy)  1225         self.remove_freebusy_for_recurrences(freebusy)  1226         self.store.set_freebusy(self.user, freebusy)  1227   1228         if self.publisher and self.is_sharing() and self.is_publishing():  1229             self.publisher.set_freebusy(self.user, freebusy)  1230   1231         # Update free/busy provider information if the event may recur  1232         # indefinitely.  1233   1234         if self.possibly_recurring_indefinitely():  1235             self.store.remove_freebusy_provider(self.user, self.obj)  1236   1237     def update_event_in_freebusy_offers(self):  1238   1239         "Update free/busy offers when handling an object."  1240   1241         freebusy = self.store.get_freebusy_offers(self.user)  1242   1243         # Obtain the attendance attributes for this user, if available.  1244   1245         self.update_freebusy_for_participant(freebusy, self.user, offer=True)  1246   1247         # Remove original recurrence details replaced by additional  1248         # recurrences, as well as obsolete additional recurrences.  1249   1250         self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))  1251         self.store.set_freebusy_offers(self.user, freebusy)  1252   1253         return True  1254   1255     def remove_event_from_freebusy_offers(self):  1256   1257         "Remove free/busy offers when handling an object."  1258   1259         freebusy = self.store.get_freebusy_offers(self.user)  1260   1261         self.remove_from_freebusy(freebusy)  1262         self.remove_freebusy_for_recurrences(freebusy)  1263         self.store.set_freebusy_offers(self.user, freebusy)  1264   1265         return True  1266   1267     # Convenience methods for removing counter-proposals and updating the  1268     # request queue.  1269   1270     def remove_request(self):  1271         return self.store.dequeue_request(self.user, self.uid, self.recurrenceid)  1272   1273     def remove_event(self):  1274         return self.store.remove_event(self.user, self.uid, self.recurrenceid)  1275   1276     def remove_counter(self, attendee):  1277         self.remove_counters([attendee])  1278   1279     def remove_counters(self, attendees):  1280         for attendee in attendees:  1281             self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid)  1282   1283         if not self.store.get_counters(self.user, self.uid, self.recurrenceid):  1284             self.store.dequeue_request(self.user, self.uid, self.recurrenceid)  1285   1286 # vim: tabstop=4 expandtab shiftwidth=4