imip-agent

imip_store.py

768:26fd43fa7db3
2015-09-26 Paul Boddie Moved id attributes to more appropriate elements. imipweb-client-simplification
     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.dates import format_datetime, get_datetime, to_timezone    26 from imiptools.filesys import fix_permissions, FileBase    27 from imiptools.period import FreeBusyPeriod    28 from os.path import exists, isfile, join    29 from os import listdir, remove, rmdir    30 from time import sleep    31 import codecs    32     33 class FileStore(FileBase):    34     35     "A file store of tabular free/busy data and objects."    36     37     def __init__(self, store_dir=None):    38         FileBase.__init__(self, store_dir or STORE_DIR)    39     40     def acquire_lock(self, user, timeout=None):    41         FileBase.acquire_lock(self, timeout, user)    42     43     def release_lock(self, user):    44         FileBase.release_lock(self, user)    45     46     # Utility methods.    47     48     def _set_defaults(self, t, empty_defaults):    49         for i, default in empty_defaults:    50             if i >= len(t):    51                 t += [None] * (i - len(t) + 1)    52             if not t[i]:    53                 t[i] = default    54         return t    55     56     def _get_table(self, user, filename, empty_defaults=None):    57     58         """    59         From the file for the given 'user' having the given 'filename', return    60         a list of tuples representing the file's contents.    61     62         The 'empty_defaults' is a list of (index, value) tuples indicating the    63         default value where a column either does not exist or provides an empty    64         value.    65         """    66     67         f = codecs.open(filename, "rb", encoding="utf-8")    68         try:    69             l = []    70             for line in f.readlines():    71                 t = line.strip(" \r\n").split("\t")    72                 if empty_defaults:    73                     t = self._set_defaults(t, empty_defaults)    74                 l.append(tuple(t))    75             return l    76         finally:    77             f.close()    78     79     def _get_table_atomic(self, user, filename, empty_defaults=None):    80     81         """    82         From the file for the given 'user' having the given 'filename', return    83         a list of tuples representing the file's contents.    84     85         The 'empty_defaults' is a list of (index, value) tuples indicating the    86         default value where a column either does not exist or provides an empty    87         value.    88         """    89     90         self.acquire_lock(user)    91         try:    92             return self._get_table(user, filename, empty_defaults)    93         finally:    94             self.release_lock(user)    95     96     def _set_table(self, user, filename, items, empty_defaults=None):    97     98         """    99         For the given 'user', write to the file having the given 'filename' the   100         'items'.   101    102         The 'empty_defaults' is a list of (index, value) tuples indicating the   103         default value where a column either does not exist or provides an empty   104         value.   105         """   106    107         f = codecs.open(filename, "wb", encoding="utf-8")   108         try:   109             for item in items:   110                 self._set_table_item(f, item, empty_defaults)   111         finally:   112             f.close()   113             fix_permissions(filename)   114    115     def _set_table_item(self, f, item, empty_defaults=None):   116    117         "Set in table 'f' the given 'item', using any 'empty_defaults'."   118    119         if empty_defaults:   120             item = self._set_defaults(list(item), empty_defaults)   121         f.write("\t".join(item) + "\n")   122    123     def _set_table_atomic(self, user, filename, items, empty_defaults=None):   124    125         """   126         For the given 'user', write to the file having the given 'filename' the   127         'items'.   128    129         The 'empty_defaults' is a list of (index, value) tuples indicating the   130         default value where a column either does not exist or provides an empty   131         value.   132         """   133    134         self.acquire_lock(user)   135         try:   136             self._set_table(user, filename, items, empty_defaults)   137         finally:   138             self.release_lock(user)   139    140     # Store object access.   141    142     def _get_object(self, user, filename):   143    144         """   145         Return the parsed object for the given 'user' having the given   146         'filename'.   147         """   148    149         self.acquire_lock(user)   150         try:   151             f = open(filename, "rb")   152             try:   153                 return parse_object(f, "utf-8")   154             finally:   155                 f.close()   156         finally:   157             self.release_lock(user)   158    159     def _set_object(self, user, filename, node):   160    161         """   162         Set an object for the given 'user' having the given 'filename', using   163         'node' to define the object.   164         """   165    166         self.acquire_lock(user)   167         try:   168             f = open(filename, "wb")   169             try:   170                 to_stream(f, node)   171             finally:   172                 f.close()   173                 fix_permissions(filename)   174         finally:   175             self.release_lock(user)   176    177         return True   178    179     def _remove_object(self, filename):   180    181         "Remove the object with the given 'filename'."   182    183         try:   184             remove(filename)   185         except OSError:   186             return False   187    188         return True   189    190     def _remove_collection(self, filename):   191    192         "Remove the collection with the given 'filename'."   193    194         try:   195             rmdir(filename)   196         except OSError:   197             return False   198    199         return True   200    201     # User discovery.   202    203     def get_users(self):   204    205         "Return a list of users."   206    207         return listdir(self.store_dir)   208    209     # Event and event metadata access.   210    211     def get_events(self, user):   212    213         "Return a list of event identifiers."   214    215         filename = self.get_object_in_store(user, "objects")   216         if not filename or not exists(filename):   217             return None   218    219         return [name for name in listdir(filename) if isfile(join(filename, name))]   220    221     def get_all_events(self, user):   222    223         "Return a set of (uid, recurrenceid) tuples for all events."   224    225         uids = self.get_events(user)   226         if not uids:   227             return set()   228    229         all_events = set()   230         for uid in uids:   231             all_events.add((uid, None))   232             all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])   233    234         return all_events   235    236     def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):   237    238         """   239         Get the filename providing the event for the given 'user' with the given   240         'uid'. If the optional 'recurrenceid' is specified, a specific instance   241         or occurrence of an event is returned.   242    243         Where 'dirname' is specified, the given directory name is used as the   244         base of the location within which any filename will reside.   245         """   246    247         if recurrenceid:   248             return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)   249         else:   250             return self.get_complete_event_filename(user, uid, dirname, username)   251    252     def get_event(self, user, uid, recurrenceid=None):   253    254         """   255         Get the event for the given 'user' with the given 'uid'. If   256         the optional 'recurrenceid' is specified, a specific instance or   257         occurrence of an event is returned.   258         """   259    260         filename = self.get_event_filename(user, uid, recurrenceid)   261         if not filename or not exists(filename):   262             return None   263    264         return filename and self._get_object(user, filename)   265    266     def get_complete_event_filename(self, user, uid, dirname=None, username=None):   267    268         """   269         Get the filename providing the event for the given 'user' with the given   270         'uid'.    271    272         Where 'dirname' is specified, the given directory name is used as the   273         base of the location within which any filename will reside.   274    275         Where 'username' is specified, the event details will reside in a file   276         bearing that name within a directory having 'uid' as its name.   277         """   278    279         return self.get_object_in_store(user, dirname, "objects", uid, username)   280    281     def get_complete_event(self, user, uid):   282    283         "Get the event for the given 'user' with the given 'uid'."   284    285         filename = self.get_complete_event_filename(user, uid)   286         if not filename or not exists(filename):   287             return None   288    289         return filename and self._get_object(user, filename)   290    291     def set_event(self, user, uid, recurrenceid, node):   292    293         """   294         Set an event for 'user' having the given 'uid' and 'recurrenceid' (which   295         if the latter is specified, a specific instance or occurrence of an   296         event is referenced), using the given 'node' description.   297         """   298    299         if recurrenceid:   300             return self.set_recurrence(user, uid, recurrenceid, node)   301         else:   302             return self.set_complete_event(user, uid, node)   303    304     def set_complete_event(self, user, uid, node):   305    306         "Set an event for 'user' having the given 'uid' and 'node'."   307    308         filename = self.get_object_in_store(user, "objects", uid)   309         if not filename:   310             return False   311    312         return self._set_object(user, filename, node)   313    314     def remove_event(self, user, uid, recurrenceid=None):   315    316         """   317         Remove an event for 'user' having the given 'uid'. If the optional   318         'recurrenceid' is specified, a specific instance or occurrence of an   319         event is removed.   320         """   321    322         if recurrenceid:   323             return self.remove_recurrence(user, uid, recurrenceid)   324         else:   325             for recurrenceid in self.get_recurrences(user, uid) or []:   326                 self.remove_recurrence(user, uid, recurrenceid)   327             return self.remove_complete_event(user, uid)   328    329     def remove_complete_event(self, user, uid):   330    331         "Remove an event for 'user' having the given 'uid'."   332    333         self.remove_recurrences(user, uid)   334    335         filename = self.get_object_in_store(user, "objects", uid)   336         if not filename:   337             return False   338    339         return self._remove_object(filename)   340    341     def get_recurrences(self, user, uid):   342    343         """   344         Get additional event instances for an event of the given 'user' with the   345         indicated 'uid'. Both active and cancelled recurrences are returned.   346         """   347    348         return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)   349    350     def get_active_recurrences(self, user, uid):   351    352         """   353         Get additional event instances for an event of the given 'user' with the   354         indicated 'uid'. Cancelled recurrences are not returned.   355         """   356    357         filename = self.get_object_in_store(user, "recurrences", uid)   358         if not filename or not exists(filename):   359             return []   360    361         return [name for name in listdir(filename) if isfile(join(filename, name))]   362    363     def get_cancelled_recurrences(self, user, uid):   364    365         """   366         Get additional event instances for an event of the given 'user' with the   367         indicated 'uid'. Only cancelled recurrences are returned.   368         """   369    370         filename = self.get_object_in_store(user, "cancelled", "recurrences", uid)   371         if not filename or not exists(filename):   372             return []   373    374         return [name for name in listdir(filename) if isfile(join(filename, name))]   375    376     def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):   377    378         """   379         For the event of the given 'user' with the given 'uid', return the   380         filename providing the recurrence with the given 'recurrenceid'.   381    382         Where 'dirname' is specified, the given directory name is used as the   383         base of the location within which any filename will reside.   384    385         Where 'username' is specified, the event details will reside in a file   386         bearing that name within a directory having 'uid' as its name.   387         """   388    389         return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)   390    391     def get_recurrence(self, user, uid, recurrenceid):   392    393         """   394         For the event of the given 'user' with the given 'uid', return the   395         specific recurrence indicated by the 'recurrenceid'.   396         """   397    398         filename = self.get_recurrence_filename(user, uid, recurrenceid)   399         if not filename or not exists(filename):   400             return None   401    402         return filename and self._get_object(user, filename)   403    404     def set_recurrence(self, user, uid, recurrenceid, node):   405    406         "Set an event for 'user' having the given 'uid' and 'node'."   407    408         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   409         if not filename:   410             return False   411    412         return self._set_object(user, filename, node)   413    414     def remove_recurrence(self, user, uid, recurrenceid):   415    416         """   417         Remove a special recurrence from an event stored by 'user' having the   418         given 'uid' and 'recurrenceid'.   419         """   420    421         filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)   422         if not filename:   423             return False   424    425         return self._remove_object(filename)   426    427     def remove_recurrences(self, user, uid):   428    429         """   430         Remove all recurrences for an event stored by 'user' having the given   431         'uid'.   432         """   433    434         for recurrenceid in self.get_recurrences(user, uid):   435             self.remove_recurrence(user, uid, recurrenceid)   436    437         recurrences = self.get_object_in_store(user, "recurrences", uid)   438         if recurrences:   439             return self._remove_collection(recurrences)   440    441         return True   442    443     # Free/busy period providers, upon extension of the free/busy records.   444    445     def _get_freebusy_providers(self, user):   446    447         """   448         Return the free/busy providers for the given 'user'.   449    450         This function returns any stored datetime and a list of providers as a   451         2-tuple. Each provider is itself a (uid, recurrenceid) tuple.   452         """   453    454         filename = self.get_object_in_store(user, "freebusy-providers")   455         if not filename or not exists(filename):   456             return None   457    458         # Attempt to read providers, with a declaration of the datetime   459         # from which such providers are considered as still being active.   460    461         t = self._get_table_atomic(user, filename, [(1, None)])   462         try:   463             dt_string = t[0][0]   464         except IndexError:   465             return None   466    467         return dt_string, t[1:]   468    469     def get_freebusy_providers(self, user, dt=None):   470    471         """   472         Return a set of uncancelled events of the form (uid, recurrenceid)   473         providing free/busy details beyond the given datetime 'dt'.   474    475         If 'dt' is not specified, all events previously found to provide   476         details will be returned. Otherwise, if 'dt' is earlier than the   477         datetime recorded for the known providers, None is returned, indicating   478         that the list of providers must be recomputed.   479    480         This function returns a list of (uid, recurrenceid) tuples upon success.   481         """   482    483         t = self._get_freebusy_providers(user)   484         if not t:   485             return None   486    487         dt_string, t = t   488    489         # If the requested datetime is earlier than the stated datetime, the   490         # providers will need to be recomputed.   491    492         if dt:   493             providers_dt = get_datetime(dt_string)   494             if not providers_dt or providers_dt > dt:   495                 return None   496    497         # Otherwise, return the providers.   498    499         return t[1:]   500    501     def _set_freebusy_providers(self, user, dt_string, t):   502    503         "Set the given provider timestamp 'dt_string' and table 't'."   504    505         filename = self.get_object_in_store(user, "freebusy-providers")   506         if not filename:   507             return False   508    509         t.insert(0, (dt_string,))   510         self._set_table_atomic(user, filename, t, [(1, "")])   511         return True   512    513     def set_freebusy_providers(self, user, dt, providers):   514    515         """   516         Define the uncancelled events providing free/busy details beyond the   517         given datetime 'dt'.   518         """   519    520         t = []   521    522         for obj in providers:   523             t.append((obj.get_uid(), obj.get_recurrenceid()))   524    525         return self._set_freebusy_providers(user, format_datetime(dt), t)   526    527     def append_freebusy_provider(self, user, provider):   528    529         "For the given 'user', append the free/busy 'provider'."   530    531         t = self._get_freebusy_providers(user)   532         if not t:   533             return False   534    535         dt_string, t = t   536         t.append((provider.get_uid(), provider.get_recurrenceid()))   537    538         return self._set_freebusy_providers(user, dt_string, t)   539    540     def remove_freebusy_provider(self, user, provider):   541    542         "For the given 'user', remove the free/busy 'provider'."   543    544         t = self._get_freebusy_providers(user)   545         if not t:   546             return False   547    548         dt_string, t = t   549         try:   550             t.remove((provider.get_uid(), provider.get_recurrenceid()))   551         except ValueError:   552             return False   553    554         return self._set_freebusy_providers(user, dt_string, t)   555    556     # Free/busy period access.   557    558     def get_freebusy(self, user, name=None, get_table=None):   559    560         "Get free/busy details for the given 'user'."   561    562         filename = self.get_object_in_store(user, name or "freebusy")   563         if not filename or not exists(filename):   564             return []   565         else:   566             return map(lambda t: FreeBusyPeriod(*t),   567                 (get_table or self._get_table_atomic)(user, filename, [(4, None)]))   568    569     def get_freebusy_for_other(self, user, other, get_table=None):   570    571         "For the given 'user', get free/busy details for the 'other' user."   572    573         filename = self.get_object_in_store(user, "freebusy-other", other)   574         if not filename or not exists(filename):   575             return []   576         else:   577             return map(lambda t: FreeBusyPeriod(*t),   578                 (get_table or self._get_table_atomic)(user, filename, [(4, None)]))   579    580     def set_freebusy(self, user, freebusy, name=None, set_table=None):   581    582         "For the given 'user', set 'freebusy' details."   583    584         filename = self.get_object_in_store(user, name or "freebusy")   585         if not filename:   586             return False   587    588         (set_table or self._set_table_atomic)(user, filename,   589             map(lambda fb: fb.as_tuple(strings_only=True), freebusy))   590         return True   591    592     def set_freebusy_for_other(self, user, freebusy, other, set_table=None):   593    594         "For the given 'user', set 'freebusy' details for the 'other' user."   595    596         filename = self.get_object_in_store(user, "freebusy-other", other)   597         if not filename:   598             return False   599    600         (set_table or self._set_table_atomic)(user, filename,   601             map(lambda fb: fb.as_tuple(strings_only=True), freebusy))   602         return True   603    604     # Tentative free/busy periods related to countering.   605    606     def get_freebusy_offers(self, user):   607    608         "Get free/busy offers for the given 'user'."   609    610         offers = []   611         expired = []   612         now = to_timezone(datetime.utcnow(), "UTC")   613    614         # Expire old offers and save the collection if modified.   615    616         self.acquire_lock(user)   617         try:   618             l = self.get_freebusy(user, "freebusy-offers")   619             for fb in l:   620                 if fb.expires and get_datetime(fb.expires) <= now:   621                     expired.append(fb)   622                 else:   623                     offers.append(fb)   624    625             if expired:   626                 self.set_freebusy_offers(user, offers)   627         finally:   628             self.release_lock(user)   629    630         return offers   631    632     def set_freebusy_offers(self, user, freebusy):   633    634         "For the given 'user', set 'freebusy' offers."   635    636         return self.set_freebusy(user, freebusy, "freebusy-offers")   637    638     # Requests and counter-proposals.   639    640     def _get_requests(self, user, queue):   641    642         "Get requests for the given 'user' from the given 'queue'."   643    644         filename = self.get_object_in_store(user, queue)   645         if not filename or not exists(filename):   646             return None   647    648         return self._get_table_atomic(user, filename, [(1, None), (2, None)])   649    650     def get_requests(self, user):   651    652         "Get requests for the given 'user'."   653    654         return self._get_requests(user, "requests")   655    656     def _set_requests(self, user, requests, queue):   657    658         """   659         For the given 'user', set the list of queued 'requests' in the given   660         'queue'.   661         """   662    663         filename = self.get_object_in_store(user, queue)   664         if not filename:   665             return False   666    667         self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])   668         return True   669    670     def set_requests(self, user, requests):   671    672         "For the given 'user', set the list of queued 'requests'."   673    674         return self._set_requests(user, requests, "requests")   675    676     def _set_request(self, user, request, queue):   677    678         """   679         For the given 'user', set the given 'request' in the given 'queue'.   680         """   681    682         filename = self.get_object_in_store(user, queue)   683         if not filename:   684             return False   685    686         self.acquire_lock(user)   687         try:   688             f = codecs.open(filename, "ab", encoding="utf-8")   689             try:   690                 self._set_table_item(f, request, [(1, ""), (2, "")])   691             finally:   692                 f.close()   693                 fix_permissions(filename)   694         finally:   695             self.release_lock(user)   696    697         return True   698    699     def set_request(self, user, uid, recurrenceid=None, type=None):   700    701         """   702         For the given 'user', set the queued 'uid' and 'recurrenceid',   703         indicating a request, along with any given 'type'.   704         """   705    706         return self._set_request(user, (uid, recurrenceid, type), "requests")   707    708     def queue_request(self, user, uid, recurrenceid=None, type=None):   709    710         """   711         Queue a request for 'user' having the given 'uid'. If the optional   712         'recurrenceid' is specified, the entry refers to a specific instance   713         or occurrence of an event. The 'type' parameter can be used to indicate   714         a specific type of request.   715         """   716    717         requests = self.get_requests(user) or []   718    719         if not self.have_request(requests, uid, recurrenceid):   720             return self.set_request(user, uid, recurrenceid, type)   721    722         return False   723    724     def dequeue_request(self, user, uid, recurrenceid=None, type=None):   725    726         """   727         Dequeue all requests for 'user' having the given 'uid'. If the optional   728         'recurrenceid' is specified, all requests for that specific instance or   729         occurrence of an event are dequeued.   730         """   731    732         requests = self.get_requests(user) or []   733         result = []   734    735         for request in requests:   736             if request[:2] == (uid, recurrenceid):   737    738                 # Remove associated objects.   739    740                 type = request[2]   741                 if type == "COUNTER":   742                     self.remove_counters(user, uid, recurrenceid)   743    744             else:   745                 result.append(request)   746    747         self.set_requests(user, result)   748         return True   749    750     def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):   751    752         """   753         Return whether 'requests' contains a request with the given 'uid' and   754         any specified 'recurrenceid' and 'type'. If 'strict' is set to a true   755         value, the precise type of the request must match; otherwise, any type   756         of request for the identified object may be matched.   757         """   758    759         for request in requests:   760             if request[:2] == (uid, recurrenceid) and (   761                 not strict or   762                 not request[2:] and not type or   763                 request[2:] and request[2] == type):   764    765                 return True   766    767         return False   768    769     def get_counters(self, user, uid, recurrenceid=None):   770    771         """   772         For the given 'user', return a list of users from whom counter-proposals   773         have been received for the given 'uid' and optional 'recurrenceid'.   774         """   775    776         filename = self.get_event_filename(user, uid, recurrenceid, "counters")   777         if not filename:   778             return False   779    780         return [name for name in listdir(filename) if isfile(join(filename, name))]   781    782     def get_counter(self, user, other, uid, recurrenceid=None):   783    784         """   785         For the given 'user', return the counter-proposal from 'other' for the   786         given 'uid' and optional 'recurrenceid'.   787         """   788    789         filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)   790         if not filename:   791             return False   792    793         return self._get_object(user, filename)   794    795     def set_counter(self, user, other, node, uid, recurrenceid=None):   796    797         """   798         For the given 'user', store a counter-proposal received from 'other' the   799         given 'node' representing that proposal for the given 'uid' and   800         'recurrenceid'.   801         """   802    803         filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)   804         if not filename:   805             return False   806    807         return self._set_object(user, filename, node)   808    809     def remove_counters(self, user, uid, recurrenceid=None):   810    811         """   812         For the given 'user', remove all counter-proposals associated with the   813         given 'uid' and 'recurrenceid'.   814         """   815    816         filename = self.get_event_filename(user, uid, recurrenceid, "counters")   817         if not filename:   818             return False   819    820         removed = False   821    822         for other in listdir(filename):   823             counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)   824             removed = removed or self._remove_object(counter_filename)   825    826         return removed   827    828     def remove_counter(self, user, other, uid, recurrenceid=None):   829    830         """   831         For the given 'user', remove any counter-proposal from 'other'   832         associated with the given 'uid' and 'recurrenceid'.   833         """   834    835         filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)   836         if not filename:   837             return False   838    839         return self._remove_object(filename)   840    841     # Event cancellation.   842    843     def cancel_event(self, user, uid, recurrenceid=None):   844    845         """   846         Cancel an event for 'user' having the given 'uid'. If the optional   847         'recurrenceid' is specified, a specific instance or occurrence of an   848         event is cancelled.   849         """   850    851         filename = self.get_event_filename(user, uid, recurrenceid)   852         cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")   853    854         if filename and cancelled_filename and exists(filename):   855             return self.move_object(filename, cancelled_filename)   856    857         return False   858    859 class FilePublisher(FileBase):   860    861     "A publisher of objects."   862    863     def __init__(self, store_dir=None):   864         FileBase.__init__(self, store_dir or PUBLISH_DIR)   865    866     def set_freebusy(self, user, freebusy):   867    868         "For the given 'user', set 'freebusy' details."   869    870         filename = self.get_object_in_store(user, "freebusy")   871         if not filename:   872             return False   873    874         record = []   875         rwrite = record.append   876    877         rwrite(("ORGANIZER", {}, user))   878         rwrite(("UID", {}, user))   879         rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))   880    881         for fb in freebusy:   882             if not fb.transp or fb.transp == "OPAQUE":   883                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(   884                     map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))   885    886         f = open(filename, "wb")   887         try:   888             to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))   889         finally:   890             f.close()   891             fix_permissions(filename)   892    893         return True   894    895 # vim: tabstop=4 expandtab shiftwidth=4