imip-agent

imip_store.py

491:c2eae45fba5d
2015-04-05 Paul Boddie Fixed attendance recording in the free/busy details for outgoing messages, properly testing for organiser-only events.
     1 #!/usr/bin/env python     2      3 """     4 A simple filesystem-based store of calendar data.     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    23 from imiptools.config import STORE_DIR, PUBLISH_DIR    24 from imiptools.data import make_calendar, parse_object, to_stream    25 from imiptools.filesys import fix_permissions, FileBase    26 from imiptools.period import FreeBusyPeriod    27 from os.path import exists, isfile, join    28 from os import listdir, remove, rmdir    29 from time import sleep    30 import codecs    31     32 class FileStore(FileBase):    33     34     "A file store of tabular free/busy data and objects."    35     36     def __init__(self, store_dir=STORE_DIR):    37         FileBase.__init__(self, store_dir)    38     39     def acquire_lock(self, user, timeout=None):    40         FileBase.acquire_lock(self, timeout, user)    41     42     def release_lock(self, user):    43         FileBase.release_lock(self, user)    44     45     def _set_defaults(self, t, empty_defaults):    46         for i, default in empty_defaults:    47             if i >= len(t):    48                 t += [None] * (i - len(t) + 1)    49             if not t[i]:    50                 t[i] = default    51         return t    52     53     def _get_table(self, user, filename, empty_defaults=None):    54     55         """    56         From the file for the given 'user' having the given 'filename', return    57         a list of tuples representing the file's contents.    58     59         The 'empty_defaults' is a list of (index, value) tuples indicating the    60         default value where a column either does not exist or provides an empty    61         value.    62         """    63     64         self.acquire_lock(user)    65         try:    66             f = codecs.open(filename, "rb", encoding="utf-8")    67             try:    68                 l = []    69                 for line in f.readlines():    70                     t = line.strip().split("\t")    71                     if empty_defaults:    72                         t = self._set_defaults(t, empty_defaults)    73                     l.append(tuple(t))    74                 return l    75             finally:    76                 f.close()    77         finally:    78             self.release_lock(user)    79     80     def _set_table(self, user, filename, items, empty_defaults=None):    81     82         """    83         For the given 'user', write to the file having the given 'filename' the    84         'items'.    85     86         The 'empty_defaults' is a list of (index, value) tuples indicating the    87         default value where a column either does not exist or provides an empty    88         value.    89         """    90     91         self.acquire_lock(user)    92         try:    93             f = codecs.open(filename, "wb", encoding="utf-8")    94             try:    95                 for item in items:    96                     if empty_defaults:    97                         item = self._set_defaults(list(item), empty_defaults)    98                     f.write("\t".join(item) + "\n")    99             finally:   100                 f.close()   101                 fix_permissions(filename)   102         finally:   103             self.release_lock(user)   104    105     def _get_object(self, user, filename):   106    107         """   108         Return the parsed object for the given 'user' having the given   109         'filename'.   110         """   111    112         self.acquire_lock(user)   113         try:   114             f = open(filename, "rb")   115             try:   116                 return parse_object(f, "utf-8")   117             finally:   118                 f.close()   119         finally:   120             self.release_lock(user)   121    122     def _set_object(self, user, filename, node):   123    124         """   125         Set an object for the given 'user' having the given 'filename', using   126         'node' to define the object.   127         """   128    129         self.acquire_lock(user)   130         try:   131             f = open(filename, "wb")   132             try:   133                 to_stream(f, node)   134             finally:   135                 f.close()   136                 fix_permissions(filename)   137         finally:   138             self.release_lock(user)   139    140         return True   141    142     def _remove_object(self, filename):   143    144         "Remove the object with the given 'filename'."   145    146         try:   147             remove(filename)   148         except OSError:   149             return False   150    151         return True   152    153     def _remove_collection(self, filename):   154    155         "Remove the collection with the given 'filename'."   156    157         try:   158             rmdir(filename)   159         except OSError:   160             return False   161    162         return True   163    164     def get_events(self, user):   165    166         "Return a list of event identifiers."   167    168         filename = self.get_object_in_store(user, "objects")   169         if not filename or not exists(filename):   170             return None   171    172         return [name for name in listdir(filename) if isfile(join(filename, name))]   173    174     def get_event(self, user, uid, recurrenceid=None):   175    176         """   177         Get the event for the given 'user' with the given 'uid'. If   178         the optional 'recurrenceid' is specified, a specific instance or   179         occurrence of an event is returned.   180         """   181    182         if recurrenceid:   183             return self.get_recurrence(user, uid, recurrenceid)   184         else:   185             return self.get_complete_event(user, uid)   186    187     def get_complete_event(self, user, uid):   188    189         "Get the event for the given 'user' with the given 'uid'."   190    191         filename = self.get_object_in_store(user, "objects", uid)   192         if not filename or not exists(filename):   193             return None   194    195         return self._get_object(user, filename)   196    197     def set_event(self, user, uid, recurrenceid, node):   198    199         """   200         Set an event for 'user' having the given 'uid' and 'recurrenceid' (which   201         if the latter is specified, a specific instance or occurrence of an   202         event is referenced), using the given 'node' description.   203         """   204    205         if recurrenceid:   206             return self.set_recurrence(user, uid, recurrenceid, node)   207         else:   208             return self.set_complete_event(user, uid, node)   209    210     def set_complete_event(self, user, uid, node):   211    212         "Set an event for 'user' having the given 'uid' and 'node'."   213    214         filename = self.get_object_in_store(user, "objects", uid)   215         if not filename:   216             return False   217    218         return self._set_object(user, filename, node)   219    220     def remove_event(self, user, uid, recurrenceid=None):   221    222         """   223         Remove an event for 'user' having the given 'uid'. If the optional   224         'recurrenceid' is specified, a specific instance or occurrence of an   225         event is removed.   226         """   227    228         if recurrenceid:   229             return self.remove_recurrence(user, uid, recurrenceid)   230         else:   231             for recurrenceid in self.get_recurrences(user, uid) or []:   232                 self.remove_recurrence(user, uid, recurrenceid)   233             return self.remove_complete_event(user, uid)   234    235     def remove_complete_event(self, user, uid):   236    237         "Remove an event for 'user' having the given 'uid'."   238    239         self.remove_recurrences(user, uid)   240    241         filename = self.get_object_in_store(user, "objects", uid)   242         if not filename:   243             return False   244    245         return self._remove_object(filename)   246    247     def get_recurrences(self, user, uid):   248    249         """   250         Get additional event instances for an event of the given 'user' with the   251         indicated 'uid'.   252         """   253    254         filename = self.get_object_in_store(user, "recurrences", uid)   255         if not filename or not exists(filename):   256             return []   257    258         return [name for name in listdir(filename) if isfile(join(filename, name))]   259    260     def get_recurrence(self, user, uid, recurrenceid):   261    262         """   263         For the event of the given 'user' with the given 'uid', return the   264         specific recurrence indicated by the 'recurrenceid'.   265         """   266    267         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   268         if not filename or not exists(filename):   269             return None   270    271         return self._get_object(user, filename)   272    273     def set_recurrence(self, user, uid, recurrenceid, node):   274    275         "Set an event for 'user' having the given 'uid' and 'node'."   276    277         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   278         if not filename:   279             return False   280    281         return self._set_object(user, filename, node)   282    283     def remove_recurrence(self, user, uid, recurrenceid):   284    285         """   286         Remove a special recurrence from an event stored by 'user' having the   287         given 'uid' and 'recurrenceid'.   288         """   289    290         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   291         if not filename:   292             return False   293    294         return self._remove_object(filename)   295    296     def remove_recurrences(self, user, uid):   297    298         """   299         Remove all recurrences for an event stored by 'user' having the given   300         'uid'.   301         """   302    303         for recurrenceid in self.get_recurrences(user, uid):   304             self.remove_recurrence(user, uid, recurrenceid)   305    306         recurrences = self.get_object_in_store(user, "recurrences", uid)   307         if recurrences:   308             return self._remove_collection(recurrences)   309    310         return True   311    312     def get_freebusy(self, user):   313    314         "Get free/busy details for the given 'user'."   315    316         filename = self.get_object_in_store(user, "freebusy")   317         if not filename or not exists(filename):   318             return []   319         else:   320             return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))   321    322     def get_freebusy_for_other(self, user, other):   323    324         "For the given 'user', get free/busy details for the 'other' user."   325    326         filename = self.get_object_in_store(user, "freebusy-other", other)   327         if not filename or not exists(filename):   328             return []   329         else:   330             return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))   331    332     def set_freebusy(self, user, freebusy):   333    334         "For the given 'user', set 'freebusy' details."   335    336         filename = self.get_object_in_store(user, "freebusy")   337         if not filename:   338             return False   339    340         self._set_table(user, filename, map(lambda fb: fb.as_tuple(), freebusy),   341             [(3, "OPAQUE"), (4, "")])   342         return True   343    344     def set_freebusy_for_other(self, user, freebusy, other):   345    346         "For the given 'user', set 'freebusy' details for the 'other' user."   347    348         filename = self.get_object_in_store(user, "freebusy-other", other)   349         if not filename:   350             return False   351    352         self._set_table(user, filename, map(lambda fb: fb.as_tuple(), freebusy),   353             [(2, ""), (3, "OPAQUE"), (4, ""), (5, ""), (6, "")])   354         return True   355    356     def _get_requests(self, user, queue):   357    358         "Get requests for the given 'user' from the given 'queue'."   359    360         filename = self.get_object_in_store(user, queue)   361         if not filename or not exists(filename):   362             return None   363    364         return self._get_table(user, filename, [(1, None)])   365    366     def get_requests(self, user):   367    368         "Get requests for the given 'user'."   369    370         return self._get_requests(user, "requests")   371    372     def get_cancellations(self, user):   373    374         "Get cancellations for the given 'user'."   375    376         return self._get_requests(user, "cancellations")   377    378     def _set_requests(self, user, requests, queue):   379    380         """   381         For the given 'user', set the list of queued 'requests' in the given   382         'queue'.   383         """   384    385         filename = self.get_object_in_store(user, queue)   386         if not filename:   387             return False   388    389         self.acquire_lock(user)   390         try:   391             f = open(filename, "w")   392             try:   393                 for request in requests:   394                     print >>f, "\t".join([value or "" for value in request])   395             finally:   396                 f.close()   397                 fix_permissions(filename)   398         finally:   399             self.release_lock(user)   400    401         return True   402    403     def set_requests(self, user, requests):   404    405         "For the given 'user', set the list of queued 'requests'."   406    407         return self._set_requests(user, requests, "requests")   408    409     def set_cancellations(self, user, cancellations):   410    411         "For the given 'user', set the list of queued 'cancellations'."   412    413         return self._set_requests(user, cancellations, "cancellations")   414    415     def _set_request(self, user, uid, recurrenceid, queue):   416    417         """   418         For the given 'user', set the queued 'uid' and 'recurrenceid' in the   419         given 'queue'.   420         """   421    422         filename = self.get_object_in_store(user, queue)   423         if not filename:   424             return False   425    426         self.acquire_lock(user)   427         try:   428             f = open(filename, "a")   429             try:   430                 print >>f, "\t".join([uid, recurrenceid or ""])   431             finally:   432                 f.close()   433                 fix_permissions(filename)   434         finally:   435             self.release_lock(user)   436    437         return True   438    439     def set_request(self, user, uid, recurrenceid=None):   440    441         "For the given 'user', set the queued 'uid' and 'recurrenceid'."   442    443         return self._set_request(user, uid, recurrenceid, "requests")   444    445     def set_cancellation(self, user, uid, recurrenceid=None):   446    447         "For the given 'user', set the queued 'uid' and 'recurrenceid'."   448    449         return self._set_request(user, uid, recurrenceid, "cancellations")   450    451     def queue_request(self, user, uid, recurrenceid=None):   452    453         """   454         Queue a request for 'user' having the given 'uid'. If the optional   455         'recurrenceid' is specified, the request refers to a specific instance   456         or occurrence of an event.   457         """   458    459         requests = self.get_requests(user) or []   460    461         if (uid, recurrenceid) not in requests:   462             return self.set_request(user, uid, recurrenceid)   463    464         return False   465    466     def dequeue_request(self, user, uid, recurrenceid=None):   467    468         """   469         Dequeue a request for 'user' having the given 'uid'. If the optional   470         'recurrenceid' is specified, the request refers to a specific instance   471         or occurrence of an event.   472         """   473    474         requests = self.get_requests(user) or []   475    476         try:   477             requests.remove((uid, recurrenceid))   478             self.set_requests(user, requests)   479         except ValueError:   480             return False   481         else:   482             return True   483    484     def cancel_event(self, user, uid, recurrenceid=None):   485    486         """   487         Queue an event for cancellation for 'user' having the given 'uid'. If   488         the optional 'recurrenceid' is specified, a specific instance or   489         occurrence of an event is cancelled.   490         """   491    492         cancellations = self.get_cancellations(user) or []   493    494         if (uid, recurrenceid) not in cancellations:   495             return self.set_cancellation(user, uid, recurrenceid)   496    497         return False   498    499 class FilePublisher(FileBase):   500    501     "A publisher of objects."   502    503     def __init__(self, store_dir=PUBLISH_DIR):   504         FileBase.__init__(self, store_dir)   505    506     def set_freebusy(self, user, freebusy):   507    508         "For the given 'user', set 'freebusy' details."   509    510         filename = self.get_object_in_store(user, "freebusy")   511         if not filename:   512             return False   513    514         record = []   515         rwrite = record.append   516    517         rwrite(("ORGANIZER", {}, user))   518         rwrite(("UID", {}, user))   519         rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))   520    521         for fb in freebusy:   522             if not fb.transp or fb.transp == "OPAQUE":   523                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([fb.start, fb.end])))   524    525         f = open(filename, "wb")   526         try:   527             to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))   528         finally:   529             f.close()   530             fix_permissions(filename)   531    532         return True   533    534 # vim: tabstop=4 expandtab shiftwidth=4