imip-agent

imiptools/data.py

291:e28bfb8b0ca1
2015-02-08 Paul Boddie Convert periods to complete UTC datetimes for free/busy recording and sharing. Thus, dates should never appear in free/busy data because they ambiguously define time periods, but this requires a time regime/zone interpretation to occur, and this is done using either a TZID record in each user's preferences or the system timezone, assuming that users are more likely to have a connection to the system they are using than some arbitrary time zone such as UTC. Fixed busy time display in the manager for periods without event information.
     1 #!/usr/bin/env python     2      3 """     4 Interpretation of vCalendar content.     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 datetime import datetime, timedelta    23 from email.mime.text import MIMEText    24 from imiptools.dates import format_datetime, get_datetime, get_freebusy_period, \    25                             to_utc_datetime    26 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node    27 from vRecurrence import get_parameters, get_rule    28 import email.utils    29     30 try:    31     from cStringIO import StringIO    32 except ImportError:    33     from StringIO import StringIO    34     35 class Object:    36     37     "Access to calendar structures."    38     39     def __init__(self, fragment):    40         self.objtype, (self.details, self.attr) = fragment.items()[0]    41     42     def get_items(self, name, all=True):    43         return get_items(self.details, name, all)    44     45     def get_item(self, name):    46         return get_item(self.details, name)    47     48     def get_value_map(self, name):    49         return get_value_map(self.details, name)    50     51     def get_values(self, name, all=True):    52         return get_values(self.details, name, all)    53     54     def get_value(self, name):    55         return get_value(self.details, name)    56     57     def get_utc_datetime(self, name):    58         return get_utc_datetime(self.details, name)    59     60     def get_datetime_item(self, name):    61         return get_datetime_item(self.details, name)    62     63     def to_node(self):    64         return to_node({self.objtype : [(self.details, self.attr)]})    65     66     def to_part(self, method):    67         return to_part(method, [self.to_node()])    68     69     # Direct access to the structure.    70     71     def __getitem__(self, name):    72         return self.details[name]    73     74     def __setitem__(self, name, value):    75         self.details[name] = value    76     77     def __delitem__(self, name):    78         del self.details[name]    79     80     # Computed results.    81     82     def get_periods(self, window_size=100):    83         return get_periods(self, window_size)    84     85     def get_periods_for_freebusy(self, tzid, window_size=100):    86         periods = self.get_periods(window_size)    87         return get_periods_for_freebusy(self, periods, tzid)    88     89 # Construction and serialisation.    90     91 def make_calendar(nodes, method=None):    92     93     """    94     Return a complete calendar node wrapping the given 'nodes' and employing the    95     given 'method', if indicated.    96     """    97     98     return ("VCALENDAR", {},    99             (method and [("METHOD", {}, method)] or []) +   100             [("VERSION", {}, "2.0")] +   101             nodes   102            )   103    104 def make_freebusy(freebusy, uid, organiser, attendee=None):   105        106     """   107     Return a calendar node defining the free/busy details described in the given   108     'freebusy' list, employing the given 'uid', for the given 'organiser', with   109     the optional 'attendee' providing recipient details.   110     """   111        112     record = []   113     rwrite = record.append   114        115     rwrite(("ORGANIZER", {}, organiser))   116    117     if attendee:   118         rwrite(("ATTENDEE", {}, attendee))    119    120     rwrite(("UID", {}, uid))   121    122     if freebusy:   123         for start, end, uid, transp in freebusy:   124             if transp == "OPAQUE":   125                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end])))   126    127     return ("VFREEBUSY", {}, record)   128    129 def parse_object(f, encoding, objtype=None):   130    131     """   132     Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is   133     given, only objects of that type will be returned. Otherwise, the root of   134     the content will be returned as a dictionary with a single key indicating   135     the object type.   136    137     Return None if the content was not readable or suitable.   138     """   139    140     try:   141         try:   142             doctype, attrs, elements = obj = parse(f, encoding=encoding)   143             if objtype and doctype == objtype:   144                 return to_dict(obj)[objtype][0]   145             elif not objtype:   146                 return to_dict(obj)   147         finally:   148             f.close()   149    150     # NOTE: Handle parse errors properly.   151    152     except (ParseError, ValueError):   153         pass   154    155     return None   156    157 def to_part(method, calendar):   158    159     """   160     Write using the given 'method', the 'calendar' details to a MIME   161     text/calendar part.   162     """   163    164     encoding = "utf-8"   165     out = StringIO()   166     try:   167         to_stream(out, make_calendar(calendar, method), encoding)   168         part = MIMEText(out.getvalue(), "calendar", encoding)   169         part.set_param("method", method)   170         return part   171    172     finally:   173         out.close()   174    175 def to_stream(out, fragment, encoding="utf-8"):   176     iterwrite(out, encoding=encoding).append(fragment)   177    178 # Structure access functions.   179    180 def get_items(d, name, all=True):   181    182     """   183     Get all items from 'd' for the given 'name', returning single items if   184     'all' is specified and set to a false value and if only one value is   185     present for the name. Return None if no items are found for the name or if   186     many items are found but 'all' is set to a false value.   187     """   188    189     if d.has_key(name):   190         values = d[name]   191         if all:   192             return values   193         elif len(values) == 1:   194             return values[0]   195         else:   196             return None   197     else:   198         return None   199    200 def get_item(d, name):   201     return get_items(d, name, False)   202    203 def get_value_map(d, name):   204    205     """   206     Return a dictionary for all items in 'd' having the given 'name'. The   207     dictionary will map values for the name to any attributes or qualifiers   208     that may have been present.   209     """   210    211     items = get_items(d, name)   212     if items:   213         return dict(items)   214     else:   215         return {}   216    217 def get_values(d, name, all=True):   218     if d.has_key(name):   219         values = d[name]   220         if not all and len(values) == 1:   221             return values[0][0]   222         else:   223             return map(lambda x: x[0], values)   224     else:   225         return None   226    227 def get_value(d, name):   228     return get_values(d, name, False)   229    230 def get_utc_datetime(d, name):   231     dt, attr = get_datetime_item(d, name)   232     return to_utc_datetime(dt)   233    234 def get_datetime_item(d, name):   235     value, attr = get_item(d, name)   236     return get_datetime(value, attr), attr   237    238 def get_addresses(values):   239     return [address for name, address in email.utils.getaddresses(values)]   240    241 def get_address(value):   242     return value.lower().startswith("mailto:") and value.lower()[7:] or value   243    244 def get_uri(value):   245     return value.lower().startswith("mailto:") and value.lower() or ":" in value and value or "mailto:%s" % value.lower()   246    247 def uri_dict(d):   248     return dict([(get_uri(key), value) for key, value in d.items()])   249    250 def uri_item(item):   251     return get_uri(item[0]), item[1]   252    253 def uri_items(items):   254     return [(get_uri(value), attr) for value, attr in items]   255    256 # Operations on structure data.   257    258 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, partstat_set):   259    260     """   261     Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and   262     'new_dtstamp', and the 'partstat_set' indication, whether the object   263     providing the new information is really newer than the object providing the   264     old information.   265     """   266    267     have_sequence = old_sequence is not None and new_sequence is not None   268     is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence)   269    270     have_dtstamp = old_dtstamp and new_dtstamp   271     is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp   272    273     is_old_sequence = have_sequence and (   274         int(new_sequence) < int(old_sequence) or   275         is_same_sequence and is_old_dtstamp   276         )   277    278     return is_same_sequence and partstat_set or not is_old_sequence   279    280 # NOTE: Need to expose the 100 day window for recurring events in the   281 # NOTE: configuration.   282    283 def get_periods(obj, window_size=100):   284    285     """   286     Return periods for the given object 'obj', confining materialised periods   287     to the given 'window_size' in days starting from the present moment.   288     """   289    290     dtstart = obj.get_utc_datetime("DTSTART")   291     dtend = obj.get_utc_datetime("DTEND")   292    293     # NOTE: Need also DURATION support.   294    295     duration = dtend - dtstart   296    297     # Recurrence rules create multiple instances to be checked.   298     # Conflicts may only be assessed within a period defined by policy   299     # for the agent, with instances outside that period being considered   300     # unchecked.   301    302     window_end = datetime.now() + timedelta(window_size)   303    304     # NOTE: Need also RDATE and EXDATE support.   305    306     rrule = obj.get_value("RRULE")   307    308     if rrule:   309         selector = get_rule(dtstart, rrule)   310         parameters = get_parameters(rrule)   311         periods = []   312         for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")):   313             start = datetime(*start, tzinfo=timezone("UTC"))   314             end = start + duration   315             periods.append((start, end))   316     else:   317         periods = [(dtstart, dtend)]   318    319     return periods   320    321 def get_periods_for_freebusy(obj, periods, tzid):   322    323     start, start_attr = obj.get_datetime_item("DTSTART")   324     end, end_attr = obj.get_datetime_item("DTEND")   325    326     tzid = start_attr.get("TZID") or end_attr.get("TZID") or tzid   327    328     l = []   329    330     for start, end in periods:   331         start, end = get_freebusy_period(start, end, tzid)   332         l.append((format_datetime(start), format_datetime(end)))   333    334     return l   335    336 # vim: tabstop=4 expandtab shiftwidth=4