imip-agent

Annotated imip_store.py

670:93ad1f9ce395
2015-08-31 Paul Boddie Added support/guidance for daily free/busy updates for all users using cron.
paul@2 1
#!/usr/bin/env python
paul@2 2
paul@146 3
"""
paul@146 4
A simple filesystem-based store of calendar data.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@146 20
"""
paul@146 21
paul@30 22
from datetime import datetime
paul@68 23
from imiptools.config import STORE_DIR, PUBLISH_DIR
paul@301 24
from imiptools.data import make_calendar, parse_object, to_stream
paul@652 25
from imiptools.dates import format_datetime, get_datetime
paul@147 26
from imiptools.filesys import fix_permissions, FileBase
paul@458 27
from imiptools.period import FreeBusyPeriod
paul@147 28
from os.path import exists, isfile, join
paul@343 29
from os import listdir, remove, rmdir
paul@303 30
from time import sleep
paul@395 31
import codecs
paul@15 32
paul@50 33
class FileStore(FileBase):
paul@50 34
paul@50 35
    "A file store of tabular free/busy data and objects."
paul@50 36
paul@597 37
    def __init__(self, store_dir=None):
paul@597 38
        FileBase.__init__(self, store_dir or STORE_DIR)
paul@147 39
paul@303 40
    def acquire_lock(self, user, timeout=None):
paul@303 41
        FileBase.acquire_lock(self, timeout, user)
paul@303 42
paul@303 43
    def release_lock(self, user):
paul@303 44
        FileBase.release_lock(self, user)
paul@303 45
paul@648 46
    # Utility methods.
paul@648 47
paul@343 48
    def _set_defaults(self, t, empty_defaults):
paul@343 49
        for i, default in empty_defaults:
paul@343 50
            if i >= len(t):
paul@343 51
                t += [None] * (i - len(t) + 1)
paul@343 52
            if not t[i]:
paul@343 53
                t[i] = default
paul@343 54
        return t
paul@343 55
paul@343 56
    def _get_table(self, user, filename, empty_defaults=None):
paul@343 57
paul@343 58
        """
paul@343 59
        From the file for the given 'user' having the given 'filename', return
paul@343 60
        a list of tuples representing the file's contents.
paul@343 61
paul@343 62
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@343 63
        default value where a column either does not exist or provides an empty
paul@343 64
        value.
paul@343 65
        """
paul@343 66
paul@343 67
        self.acquire_lock(user)
paul@343 68
        try:
paul@395 69
            f = codecs.open(filename, "rb", encoding="utf-8")
paul@343 70
            try:
paul@343 71
                l = []
paul@343 72
                for line in f.readlines():
paul@652 73
                    t = line.strip(" \r\n").split("\t")
paul@343 74
                    if empty_defaults:
paul@343 75
                        t = self._set_defaults(t, empty_defaults)
paul@343 76
                    l.append(tuple(t))
paul@343 77
                return l
paul@343 78
            finally:
paul@343 79
                f.close()
paul@343 80
        finally:
paul@343 81
            self.release_lock(user)
paul@343 82
paul@343 83
    def _set_table(self, user, filename, items, empty_defaults=None):
paul@343 84
paul@343 85
        """
paul@343 86
        For the given 'user', write to the file having the given 'filename' the
paul@343 87
        'items'.
paul@343 88
paul@343 89
        The 'empty_defaults' is a list of (index, value) tuples indicating the
paul@343 90
        default value where a column either does not exist or provides an empty
paul@343 91
        value.
paul@343 92
        """
paul@343 93
paul@343 94
        self.acquire_lock(user)
paul@343 95
        try:
paul@395 96
            f = codecs.open(filename, "wb", encoding="utf-8")
paul@343 97
            try:
paul@343 98
                for item in items:
paul@343 99
                    if empty_defaults:
paul@343 100
                        item = self._set_defaults(list(item), empty_defaults)
paul@343 101
                    f.write("\t".join(item) + "\n")
paul@343 102
            finally:
paul@343 103
                f.close()
paul@343 104
                fix_permissions(filename)
paul@343 105
        finally:
paul@343 106
            self.release_lock(user)
paul@343 107
paul@648 108
    # Store object access.
paul@648 109
paul@329 110
    def _get_object(self, user, filename):
paul@329 111
paul@329 112
        """
paul@329 113
        Return the parsed object for the given 'user' having the given
paul@329 114
        'filename'.
paul@329 115
        """
paul@329 116
paul@329 117
        self.acquire_lock(user)
paul@329 118
        try:
paul@329 119
            f = open(filename, "rb")
paul@329 120
            try:
paul@329 121
                return parse_object(f, "utf-8")
paul@329 122
            finally:
paul@329 123
                f.close()
paul@329 124
        finally:
paul@329 125
            self.release_lock(user)
paul@329 126
paul@329 127
    def _set_object(self, user, filename, node):
paul@329 128
paul@329 129
        """
paul@329 130
        Set an object for the given 'user' having the given 'filename', using
paul@329 131
        'node' to define the object.
paul@329 132
        """
paul@329 133
paul@329 134
        self.acquire_lock(user)
paul@329 135
        try:
paul@329 136
            f = open(filename, "wb")
paul@329 137
            try:
paul@329 138
                to_stream(f, node)
paul@329 139
            finally:
paul@329 140
                f.close()
paul@329 141
                fix_permissions(filename)
paul@329 142
        finally:
paul@329 143
            self.release_lock(user)
paul@329 144
paul@329 145
        return True
paul@329 146
paul@329 147
    def _remove_object(self, filename):
paul@329 148
paul@329 149
        "Remove the object with the given 'filename'."
paul@329 150
paul@329 151
        try:
paul@329 152
            remove(filename)
paul@329 153
        except OSError:
paul@329 154
            return False
paul@329 155
paul@329 156
        return True
paul@329 157
paul@343 158
    def _remove_collection(self, filename):
paul@343 159
paul@343 160
        "Remove the collection with the given 'filename'."
paul@343 161
paul@343 162
        try:
paul@343 163
            rmdir(filename)
paul@343 164
        except OSError:
paul@343 165
            return False
paul@343 166
paul@343 167
        return True
paul@343 168
paul@670 169
    # User discovery.
paul@670 170
paul@670 171
    def get_users(self):
paul@670 172
paul@670 173
        "Return a list of users."
paul@670 174
paul@670 175
        return listdir(self.store_dir)
paul@670 176
paul@648 177
    # Event and event metadata access.
paul@648 178
paul@119 179
    def get_events(self, user):
paul@119 180
paul@119 181
        "Return a list of event identifiers."
paul@119 182
paul@138 183
        filename = self.get_object_in_store(user, "objects")
paul@119 184
        if not filename or not exists(filename):
paul@119 185
            return None
paul@119 186
paul@119 187
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 188
paul@648 189
    def get_all_events(self, user):
paul@648 190
paul@648 191
        "Return a set of (uid, recurrenceid) tuples for all events."
paul@648 192
paul@648 193
        uids = self.get_events(user)
paul@648 194
paul@648 195
        all_events = set()
paul@648 196
        for uid in uids:
paul@648 197
            all_events.add((uid, None))
paul@648 198
            all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
paul@648 199
paul@648 200
        return all_events
paul@648 201
paul@648 202
    def get_active_events(self, user):
paul@648 203
paul@648 204
        "Return a set of uncancelled events of the form (uid, recurrenceid)."
paul@648 205
paul@648 206
        all_events = self.get_all_events(user)
paul@648 207
paul@648 208
        # Filter out cancelled events.
paul@648 209
paul@648 210
        cancelled = self.get_cancellations(user) or []
paul@648 211
        all_events.difference_update(cancelled)
paul@648 212
        return all_events
paul@648 213
paul@343 214
    def get_event(self, user, uid, recurrenceid=None):
paul@343 215
paul@343 216
        """
paul@343 217
        Get the event for the given 'user' with the given 'uid'. If
paul@343 218
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 219
        occurrence of an event is returned.
paul@343 220
        """
paul@343 221
paul@343 222
        if recurrenceid:
paul@343 223
            return self.get_recurrence(user, uid, recurrenceid)
paul@343 224
        else:
paul@343 225
            return self.get_complete_event(user, uid)
paul@343 226
paul@343 227
    def get_complete_event(self, user, uid):
paul@50 228
paul@50 229
        "Get the event for the given 'user' with the given 'uid'."
paul@50 230
paul@138 231
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 232
        if not filename or not exists(filename):
paul@50 233
            return None
paul@50 234
paul@329 235
        return self._get_object(user, filename)
paul@50 236
paul@343 237
    def set_event(self, user, uid, recurrenceid, node):
paul@343 238
paul@343 239
        """
paul@343 240
        Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
paul@343 241
        if the latter is specified, a specific instance or occurrence of an
paul@343 242
        event is referenced), using the given 'node' description.
paul@343 243
        """
paul@343 244
paul@343 245
        if recurrenceid:
paul@343 246
            return self.set_recurrence(user, uid, recurrenceid, node)
paul@343 247
        else:
paul@343 248
            return self.set_complete_event(user, uid, node)
paul@343 249
paul@343 250
    def set_complete_event(self, user, uid, node):
paul@50 251
paul@50 252
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 253
paul@138 254
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 255
        if not filename:
paul@50 256
            return False
paul@50 257
paul@329 258
        return self._set_object(user, filename, node)
paul@15 259
paul@365 260
    def remove_event(self, user, uid, recurrenceid=None):
paul@234 261
paul@343 262
        """
paul@343 263
        Remove an event for 'user' having the given 'uid'. If the optional
paul@343 264
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@343 265
        event is removed.
paul@343 266
        """
paul@343 267
paul@343 268
        if recurrenceid:
paul@343 269
            return self.remove_recurrence(user, uid, recurrenceid)
paul@343 270
        else:
paul@343 271
            for recurrenceid in self.get_recurrences(user, uid) or []:
paul@343 272
                self.remove_recurrence(user, uid, recurrenceid)
paul@343 273
            return self.remove_complete_event(user, uid)
paul@343 274
paul@343 275
    def remove_complete_event(self, user, uid):
paul@343 276
paul@234 277
        "Remove an event for 'user' having the given 'uid'."
paul@234 278
paul@378 279
        self.remove_recurrences(user, uid)
paul@369 280
paul@234 281
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 282
        if not filename:
paul@234 283
            return False
paul@234 284
paul@329 285
        return self._remove_object(filename)
paul@234 286
paul@334 287
    def get_recurrences(self, user, uid):
paul@334 288
paul@334 289
        """
paul@334 290
        Get additional event instances for an event of the given 'user' with the
paul@334 291
        indicated 'uid'.
paul@334 292
        """
paul@334 293
paul@334 294
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@334 295
        if not filename or not exists(filename):
paul@347 296
            return []
paul@334 297
paul@334 298
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 299
paul@334 300
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 301
paul@334 302
        """
paul@334 303
        For the event of the given 'user' with the given 'uid', return the
paul@334 304
        specific recurrence indicated by the 'recurrenceid'.
paul@334 305
        """
paul@334 306
paul@334 307
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 308
        if not filename or not exists(filename):
paul@334 309
            return None
paul@334 310
paul@334 311
        return self._get_object(user, filename)
paul@334 312
paul@334 313
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 314
paul@334 315
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 316
paul@334 317
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 318
        if not filename:
paul@334 319
            return False
paul@334 320
paul@334 321
        return self._set_object(user, filename, node)
paul@334 322
paul@334 323
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 324
paul@378 325
        """
paul@378 326
        Remove a special recurrence from an event stored by 'user' having the
paul@378 327
        given 'uid' and 'recurrenceid'.
paul@378 328
        """
paul@334 329
paul@378 330
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 331
        if not filename:
paul@334 332
            return False
paul@334 333
paul@334 334
        return self._remove_object(filename)
paul@334 335
paul@378 336
    def remove_recurrences(self, user, uid):
paul@378 337
paul@378 338
        """
paul@378 339
        Remove all recurrences for an event stored by 'user' having the given
paul@378 340
        'uid'.
paul@378 341
        """
paul@378 342
paul@378 343
        for recurrenceid in self.get_recurrences(user, uid):
paul@378 344
            self.remove_recurrence(user, uid, recurrenceid)
paul@378 345
paul@378 346
        recurrences = self.get_object_in_store(user, "recurrences", uid)
paul@378 347
        if recurrences:
paul@378 348
            return self._remove_collection(recurrences)
paul@378 349
paul@378 350
        return True
paul@378 351
paul@652 352
    # Free/busy period providers, upon extension of the free/busy records.
paul@652 353
paul@652 354
    def get_freebusy_providers(self, user, dt=None):
paul@652 355
paul@652 356
        """
paul@652 357
        Return a set of uncancelled events of the form (uid, recurrenceid)
paul@652 358
        providing free/busy details beyond the given datetime 'dt'.
paul@654 359
paul@654 360
        If 'dt' is not specified, all events previously found to provide
paul@654 361
        details will be returned. Otherwise, if 'dt' is earlier than the
paul@654 362
        datetime recorded for the known providers, None is returned, indicating
paul@654 363
        that the list of providers must be recomputed.
paul@652 364
        """
paul@652 365
paul@652 366
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@652 367
        if not filename or not exists(filename):
paul@652 368
            return None
paul@652 369
        else:
paul@652 370
            # Attempt to read providers, with a declaration of the datetime
paul@652 371
            # from which such providers are considered as still being active.
paul@652 372
paul@652 373
            t = self._get_table(user, filename, [(1, None)])
paul@652 374
            try:
paul@652 375
                dt_string = t[0][0]
paul@652 376
            except IndexError:
paul@652 377
                return None
paul@652 378
paul@652 379
            # If the requested datetime is earlier than the stated datetime, the
paul@652 380
            # providers will need to be recomputed.
paul@652 381
paul@652 382
            if dt:
paul@652 383
                providers_dt = get_datetime(dt_string)
paul@652 384
                if not providers_dt or providers_dt > dt:
paul@652 385
                    return None
paul@652 386
paul@652 387
            # Otherwise, return the providers.
paul@652 388
paul@652 389
            return t[1:]
paul@652 390
paul@654 391
    def set_freebusy_providers(self, user, dt, providers):
paul@654 392
paul@654 393
        """
paul@654 394
        Define the uncancelled events providing free/busy details beyond the
paul@654 395
        given datetime 'dt'.
paul@654 396
        """
paul@654 397
paul@654 398
        t = [(format_datetime(dt),)]
paul@654 399
paul@654 400
        for obj in providers:
paul@654 401
            t.append((obj.get_uid(), obj.get_recurrenceid() or ""))
paul@654 402
paul@654 403
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@654 404
        if not filename:
paul@654 405
            return False
paul@654 406
paul@654 407
        self._set_table(user, filename, t)
paul@654 408
        return True
paul@654 409
paul@648 410
    # Free/busy period access.
paul@648 411
paul@15 412
    def get_freebusy(self, user):
paul@15 413
paul@15 414
        "Get free/busy details for the given 'user'."
paul@15 415
paul@52 416
        filename = self.get_object_in_store(user, "freebusy")
paul@15 417
        if not filename or not exists(filename):
paul@167 418
            return []
paul@112 419
        else:
paul@458 420
            return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))
paul@2 421
paul@112 422
    def get_freebusy_for_other(self, user, other):
paul@112 423
paul@112 424
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 425
paul@112 426
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@167 427
        if not filename or not exists(filename):
paul@167 428
            return []
paul@112 429
        else:
paul@458 430
            return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))
paul@2 431
paul@15 432
    def set_freebusy(self, user, freebusy):
paul@15 433
paul@15 434
        "For the given 'user', set 'freebusy' details."
paul@15 435
paul@52 436
        filename = self.get_object_in_store(user, "freebusy")
paul@15 437
        if not filename:
paul@15 438
            return False
paul@15 439
paul@563 440
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@15 441
        return True
paul@15 442
paul@110 443
    def set_freebusy_for_other(self, user, freebusy, other):
paul@110 444
paul@110 445
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 446
paul@110 447
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 448
        if not filename:
paul@110 449
            return False
paul@110 450
paul@563 451
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@112 452
        return True
paul@112 453
paul@648 454
    # Object status details access.
paul@648 455
paul@142 456
    def _get_requests(self, user, queue):
paul@66 457
paul@142 458
        "Get requests for the given 'user' from the given 'queue'."
paul@66 459
paul@142 460
        filename = self.get_object_in_store(user, queue)
paul@81 461
        if not filename or not exists(filename):
paul@66 462
            return None
paul@66 463
paul@343 464
        return self._get_table(user, filename, [(1, None)])
paul@66 465
paul@142 466
    def get_requests(self, user):
paul@142 467
paul@142 468
        "Get requests for the given 'user'."
paul@142 469
paul@142 470
        return self._get_requests(user, "requests")
paul@142 471
paul@142 472
    def get_cancellations(self, user):
paul@142 473
paul@142 474
        "Get cancellations for the given 'user'."
paul@55 475
paul@142 476
        return self._get_requests(user, "cancellations")
paul@142 477
paul@142 478
    def _set_requests(self, user, requests, queue):
paul@66 479
paul@142 480
        """
paul@142 481
        For the given 'user', set the list of queued 'requests' in the given
paul@142 482
        'queue'.
paul@142 483
        """
paul@142 484
paul@142 485
        filename = self.get_object_in_store(user, queue)
paul@66 486
        if not filename:
paul@66 487
            return False
paul@66 488
paul@303 489
        self.acquire_lock(user)
paul@66 490
        try:
paul@303 491
            f = open(filename, "w")
paul@303 492
            try:
paul@303 493
                for request in requests:
paul@343 494
                    print >>f, "\t".join([value or "" for value in request])
paul@303 495
            finally:
paul@303 496
                f.close()
paul@303 497
                fix_permissions(filename)
paul@66 498
        finally:
paul@303 499
            self.release_lock(user)
paul@66 500
paul@66 501
        return True
paul@66 502
paul@142 503
    def set_requests(self, user, requests):
paul@142 504
paul@142 505
        "For the given 'user', set the list of queued 'requests'."
paul@142 506
paul@142 507
        return self._set_requests(user, requests, "requests")
paul@142 508
paul@142 509
    def set_cancellations(self, user, cancellations):
paul@66 510
paul@142 511
        "For the given 'user', set the list of queued 'cancellations'."
paul@142 512
paul@142 513
        return self._set_requests(user, cancellations, "cancellations")
paul@55 514
paul@343 515
    def _set_request(self, user, uid, recurrenceid, queue):
paul@142 516
paul@343 517
        """
paul@343 518
        For the given 'user', set the queued 'uid' and 'recurrenceid' in the
paul@343 519
        given 'queue'.
paul@343 520
        """
paul@142 521
paul@142 522
        filename = self.get_object_in_store(user, queue)
paul@55 523
        if not filename:
paul@55 524
            return False
paul@55 525
paul@303 526
        self.acquire_lock(user)
paul@55 527
        try:
paul@303 528
            f = open(filename, "a")
paul@303 529
            try:
paul@343 530
                print >>f, "\t".join([uid, recurrenceid or ""])
paul@303 531
            finally:
paul@303 532
                f.close()
paul@303 533
                fix_permissions(filename)
paul@55 534
        finally:
paul@303 535
            self.release_lock(user)
paul@55 536
paul@55 537
        return True
paul@55 538
paul@343 539
    def set_request(self, user, uid, recurrenceid=None):
paul@142 540
paul@343 541
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 542
paul@343 543
        return self._set_request(user, uid, recurrenceid, "requests")
paul@142 544
paul@343 545
    def set_cancellation(self, user, uid, recurrenceid=None):
paul@343 546
paul@343 547
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 548
paul@343 549
        return self._set_request(user, uid, recurrenceid, "cancellations")
paul@142 550
paul@343 551
    def queue_request(self, user, uid, recurrenceid=None):
paul@142 552
paul@343 553
        """
paul@343 554
        Queue a request for 'user' having the given 'uid'. If the optional
paul@343 555
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 556
        or occurrence of an event.
paul@343 557
        """
paul@66 558
paul@81 559
        requests = self.get_requests(user) or []
paul@66 560
paul@343 561
        if (uid, recurrenceid) not in requests:
paul@343 562
            return self.set_request(user, uid, recurrenceid)
paul@66 563
paul@66 564
        return False
paul@66 565
paul@343 566
    def dequeue_request(self, user, uid, recurrenceid=None):
paul@105 567
paul@343 568
        """
paul@343 569
        Dequeue a request for 'user' having the given 'uid'. If the optional
paul@343 570
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 571
        or occurrence of an event.
paul@343 572
        """
paul@105 573
paul@105 574
        requests = self.get_requests(user) or []
paul@105 575
paul@105 576
        try:
paul@343 577
            requests.remove((uid, recurrenceid))
paul@105 578
            self.set_requests(user, requests)
paul@105 579
        except ValueError:
paul@105 580
            return False
paul@105 581
        else:
paul@105 582
            return True
paul@105 583
paul@343 584
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 585
paul@343 586
        """
paul@343 587
        Queue an event for cancellation for 'user' having the given 'uid'. If
paul@343 588
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 589
        occurrence of an event is cancelled.
paul@343 590
        """
paul@142 591
paul@142 592
        cancellations = self.get_cancellations(user) or []
paul@142 593
paul@343 594
        if (uid, recurrenceid) not in cancellations:
paul@343 595
            return self.set_cancellation(user, uid, recurrenceid)
paul@142 596
paul@142 597
        return False
paul@142 598
paul@30 599
class FilePublisher(FileBase):
paul@30 600
paul@30 601
    "A publisher of objects."
paul@30 602
paul@597 603
    def __init__(self, store_dir=None):
paul@597 604
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 605
paul@30 606
    def set_freebusy(self, user, freebusy):
paul@30 607
paul@30 608
        "For the given 'user', set 'freebusy' details."
paul@30 609
paul@52 610
        filename = self.get_object_in_store(user, "freebusy")
paul@30 611
        if not filename:
paul@30 612
            return False
paul@30 613
paul@30 614
        record = []
paul@30 615
        rwrite = record.append
paul@30 616
paul@30 617
        rwrite(("ORGANIZER", {}, user))
paul@30 618
        rwrite(("UID", {}, user))
paul@30 619
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 620
paul@458 621
        for fb in freebusy:
paul@458 622
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 623
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 624
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 625
paul@395 626
        f = open(filename, "wb")
paul@30 627
        try:
paul@30 628
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 629
        finally:
paul@30 630
            f.close()
paul@103 631
            fix_permissions(filename)
paul@30 632
paul@30 633
        return True
paul@30 634
paul@2 635
# vim: tabstop=4 expandtab shiftwidth=4