imip-agent

Annotated imip_store.py

654:e5f8a8e266c1
2015-08-12 Paul Boddie Added support for writing the recurrence-providing events for use in more efficient subsequent generation of free/busy collections.
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@648 169
    # Event and event metadata access.
paul@648 170
paul@119 171
    def get_events(self, user):
paul@119 172
paul@119 173
        "Return a list of event identifiers."
paul@119 174
paul@138 175
        filename = self.get_object_in_store(user, "objects")
paul@119 176
        if not filename or not exists(filename):
paul@119 177
            return None
paul@119 178
paul@119 179
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 180
paul@648 181
    def get_all_events(self, user):
paul@648 182
paul@648 183
        "Return a set of (uid, recurrenceid) tuples for all events."
paul@648 184
paul@648 185
        uids = self.get_events(user)
paul@648 186
paul@648 187
        all_events = set()
paul@648 188
        for uid in uids:
paul@648 189
            all_events.add((uid, None))
paul@648 190
            all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
paul@648 191
paul@648 192
        return all_events
paul@648 193
paul@648 194
    def get_active_events(self, user):
paul@648 195
paul@648 196
        "Return a set of uncancelled events of the form (uid, recurrenceid)."
paul@648 197
paul@648 198
        all_events = self.get_all_events(user)
paul@648 199
paul@648 200
        # Filter out cancelled events.
paul@648 201
paul@648 202
        cancelled = self.get_cancellations(user) or []
paul@648 203
        all_events.difference_update(cancelled)
paul@648 204
        return all_events
paul@648 205
paul@343 206
    def get_event(self, user, uid, recurrenceid=None):
paul@343 207
paul@343 208
        """
paul@343 209
        Get the event for the given 'user' with the given 'uid'. If
paul@343 210
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 211
        occurrence of an event is returned.
paul@343 212
        """
paul@343 213
paul@343 214
        if recurrenceid:
paul@343 215
            return self.get_recurrence(user, uid, recurrenceid)
paul@343 216
        else:
paul@343 217
            return self.get_complete_event(user, uid)
paul@343 218
paul@343 219
    def get_complete_event(self, user, uid):
paul@50 220
paul@50 221
        "Get the event for the given 'user' with the given 'uid'."
paul@50 222
paul@138 223
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 224
        if not filename or not exists(filename):
paul@50 225
            return None
paul@50 226
paul@329 227
        return self._get_object(user, filename)
paul@50 228
paul@343 229
    def set_event(self, user, uid, recurrenceid, node):
paul@343 230
paul@343 231
        """
paul@343 232
        Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
paul@343 233
        if the latter is specified, a specific instance or occurrence of an
paul@343 234
        event is referenced), using the given 'node' description.
paul@343 235
        """
paul@343 236
paul@343 237
        if recurrenceid:
paul@343 238
            return self.set_recurrence(user, uid, recurrenceid, node)
paul@343 239
        else:
paul@343 240
            return self.set_complete_event(user, uid, node)
paul@343 241
paul@343 242
    def set_complete_event(self, user, uid, node):
paul@50 243
paul@50 244
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 245
paul@138 246
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 247
        if not filename:
paul@50 248
            return False
paul@50 249
paul@329 250
        return self._set_object(user, filename, node)
paul@15 251
paul@365 252
    def remove_event(self, user, uid, recurrenceid=None):
paul@234 253
paul@343 254
        """
paul@343 255
        Remove an event for 'user' having the given 'uid'. If the optional
paul@343 256
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@343 257
        event is removed.
paul@343 258
        """
paul@343 259
paul@343 260
        if recurrenceid:
paul@343 261
            return self.remove_recurrence(user, uid, recurrenceid)
paul@343 262
        else:
paul@343 263
            for recurrenceid in self.get_recurrences(user, uid) or []:
paul@343 264
                self.remove_recurrence(user, uid, recurrenceid)
paul@343 265
            return self.remove_complete_event(user, uid)
paul@343 266
paul@343 267
    def remove_complete_event(self, user, uid):
paul@343 268
paul@234 269
        "Remove an event for 'user' having the given 'uid'."
paul@234 270
paul@378 271
        self.remove_recurrences(user, uid)
paul@369 272
paul@234 273
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 274
        if not filename:
paul@234 275
            return False
paul@234 276
paul@329 277
        return self._remove_object(filename)
paul@234 278
paul@334 279
    def get_recurrences(self, user, uid):
paul@334 280
paul@334 281
        """
paul@334 282
        Get additional event instances for an event of the given 'user' with the
paul@334 283
        indicated 'uid'.
paul@334 284
        """
paul@334 285
paul@334 286
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@334 287
        if not filename or not exists(filename):
paul@347 288
            return []
paul@334 289
paul@334 290
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 291
paul@334 292
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 293
paul@334 294
        """
paul@334 295
        For the event of the given 'user' with the given 'uid', return the
paul@334 296
        specific recurrence indicated by the 'recurrenceid'.
paul@334 297
        """
paul@334 298
paul@334 299
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 300
        if not filename or not exists(filename):
paul@334 301
            return None
paul@334 302
paul@334 303
        return self._get_object(user, filename)
paul@334 304
paul@334 305
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 306
paul@334 307
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 308
paul@334 309
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 310
        if not filename:
paul@334 311
            return False
paul@334 312
paul@334 313
        return self._set_object(user, filename, node)
paul@334 314
paul@334 315
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 316
paul@378 317
        """
paul@378 318
        Remove a special recurrence from an event stored by 'user' having the
paul@378 319
        given 'uid' and 'recurrenceid'.
paul@378 320
        """
paul@334 321
paul@378 322
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 323
        if not filename:
paul@334 324
            return False
paul@334 325
paul@334 326
        return self._remove_object(filename)
paul@334 327
paul@378 328
    def remove_recurrences(self, user, uid):
paul@378 329
paul@378 330
        """
paul@378 331
        Remove all recurrences for an event stored by 'user' having the given
paul@378 332
        'uid'.
paul@378 333
        """
paul@378 334
paul@378 335
        for recurrenceid in self.get_recurrences(user, uid):
paul@378 336
            self.remove_recurrence(user, uid, recurrenceid)
paul@378 337
paul@378 338
        recurrences = self.get_object_in_store(user, "recurrences", uid)
paul@378 339
        if recurrences:
paul@378 340
            return self._remove_collection(recurrences)
paul@378 341
paul@378 342
        return True
paul@378 343
paul@652 344
    # Free/busy period providers, upon extension of the free/busy records.
paul@652 345
paul@652 346
    def get_freebusy_providers(self, user, dt=None):
paul@652 347
paul@652 348
        """
paul@652 349
        Return a set of uncancelled events of the form (uid, recurrenceid)
paul@652 350
        providing free/busy details beyond the given datetime 'dt'.
paul@654 351
paul@654 352
        If 'dt' is not specified, all events previously found to provide
paul@654 353
        details will be returned. Otherwise, if 'dt' is earlier than the
paul@654 354
        datetime recorded for the known providers, None is returned, indicating
paul@654 355
        that the list of providers must be recomputed.
paul@652 356
        """
paul@652 357
paul@652 358
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@652 359
        if not filename or not exists(filename):
paul@652 360
            return None
paul@652 361
        else:
paul@652 362
            # Attempt to read providers, with a declaration of the datetime
paul@652 363
            # from which such providers are considered as still being active.
paul@652 364
paul@652 365
            t = self._get_table(user, filename, [(1, None)])
paul@652 366
            try:
paul@652 367
                dt_string = t[0][0]
paul@652 368
            except IndexError:
paul@652 369
                return None
paul@652 370
paul@652 371
            # If the requested datetime is earlier than the stated datetime, the
paul@652 372
            # providers will need to be recomputed.
paul@652 373
paul@652 374
            if dt:
paul@652 375
                providers_dt = get_datetime(dt_string)
paul@652 376
                if not providers_dt or providers_dt > dt:
paul@652 377
                    return None
paul@652 378
paul@652 379
            # Otherwise, return the providers.
paul@652 380
paul@652 381
            return t[1:]
paul@652 382
paul@654 383
    def set_freebusy_providers(self, user, dt, providers):
paul@654 384
paul@654 385
        """
paul@654 386
        Define the uncancelled events providing free/busy details beyond the
paul@654 387
        given datetime 'dt'.
paul@654 388
        """
paul@654 389
paul@654 390
        t = [(format_datetime(dt),)]
paul@654 391
paul@654 392
        for obj in providers:
paul@654 393
            t.append((obj.get_uid(), obj.get_recurrenceid() or ""))
paul@654 394
paul@654 395
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@654 396
        if not filename:
paul@654 397
            return False
paul@654 398
paul@654 399
        self._set_table(user, filename, t)
paul@654 400
        return True
paul@654 401
paul@648 402
    # Free/busy period access.
paul@648 403
paul@15 404
    def get_freebusy(self, user):
paul@15 405
paul@15 406
        "Get free/busy details for the given 'user'."
paul@15 407
paul@52 408
        filename = self.get_object_in_store(user, "freebusy")
paul@15 409
        if not filename or not exists(filename):
paul@167 410
            return []
paul@112 411
        else:
paul@458 412
            return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))
paul@2 413
paul@112 414
    def get_freebusy_for_other(self, user, other):
paul@112 415
paul@112 416
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 417
paul@112 418
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@167 419
        if not filename or not exists(filename):
paul@167 420
            return []
paul@112 421
        else:
paul@458 422
            return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))
paul@2 423
paul@15 424
    def set_freebusy(self, user, freebusy):
paul@15 425
paul@15 426
        "For the given 'user', set 'freebusy' details."
paul@15 427
paul@52 428
        filename = self.get_object_in_store(user, "freebusy")
paul@15 429
        if not filename:
paul@15 430
            return False
paul@15 431
paul@563 432
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@15 433
        return True
paul@15 434
paul@110 435
    def set_freebusy_for_other(self, user, freebusy, other):
paul@110 436
paul@110 437
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 438
paul@110 439
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 440
        if not filename:
paul@110 441
            return False
paul@110 442
paul@563 443
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@112 444
        return True
paul@112 445
paul@648 446
    # Object status details access.
paul@648 447
paul@142 448
    def _get_requests(self, user, queue):
paul@66 449
paul@142 450
        "Get requests for the given 'user' from the given 'queue'."
paul@66 451
paul@142 452
        filename = self.get_object_in_store(user, queue)
paul@81 453
        if not filename or not exists(filename):
paul@66 454
            return None
paul@66 455
paul@343 456
        return self._get_table(user, filename, [(1, None)])
paul@66 457
paul@142 458
    def get_requests(self, user):
paul@142 459
paul@142 460
        "Get requests for the given 'user'."
paul@142 461
paul@142 462
        return self._get_requests(user, "requests")
paul@142 463
paul@142 464
    def get_cancellations(self, user):
paul@142 465
paul@142 466
        "Get cancellations for the given 'user'."
paul@55 467
paul@142 468
        return self._get_requests(user, "cancellations")
paul@142 469
paul@142 470
    def _set_requests(self, user, requests, queue):
paul@66 471
paul@142 472
        """
paul@142 473
        For the given 'user', set the list of queued 'requests' in the given
paul@142 474
        'queue'.
paul@142 475
        """
paul@142 476
paul@142 477
        filename = self.get_object_in_store(user, queue)
paul@66 478
        if not filename:
paul@66 479
            return False
paul@66 480
paul@303 481
        self.acquire_lock(user)
paul@66 482
        try:
paul@303 483
            f = open(filename, "w")
paul@303 484
            try:
paul@303 485
                for request in requests:
paul@343 486
                    print >>f, "\t".join([value or "" for value in request])
paul@303 487
            finally:
paul@303 488
                f.close()
paul@303 489
                fix_permissions(filename)
paul@66 490
        finally:
paul@303 491
            self.release_lock(user)
paul@66 492
paul@66 493
        return True
paul@66 494
paul@142 495
    def set_requests(self, user, requests):
paul@142 496
paul@142 497
        "For the given 'user', set the list of queued 'requests'."
paul@142 498
paul@142 499
        return self._set_requests(user, requests, "requests")
paul@142 500
paul@142 501
    def set_cancellations(self, user, cancellations):
paul@66 502
paul@142 503
        "For the given 'user', set the list of queued 'cancellations'."
paul@142 504
paul@142 505
        return self._set_requests(user, cancellations, "cancellations")
paul@55 506
paul@343 507
    def _set_request(self, user, uid, recurrenceid, queue):
paul@142 508
paul@343 509
        """
paul@343 510
        For the given 'user', set the queued 'uid' and 'recurrenceid' in the
paul@343 511
        given 'queue'.
paul@343 512
        """
paul@142 513
paul@142 514
        filename = self.get_object_in_store(user, queue)
paul@55 515
        if not filename:
paul@55 516
            return False
paul@55 517
paul@303 518
        self.acquire_lock(user)
paul@55 519
        try:
paul@303 520
            f = open(filename, "a")
paul@303 521
            try:
paul@343 522
                print >>f, "\t".join([uid, recurrenceid or ""])
paul@303 523
            finally:
paul@303 524
                f.close()
paul@303 525
                fix_permissions(filename)
paul@55 526
        finally:
paul@303 527
            self.release_lock(user)
paul@55 528
paul@55 529
        return True
paul@55 530
paul@343 531
    def set_request(self, user, uid, recurrenceid=None):
paul@142 532
paul@343 533
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 534
paul@343 535
        return self._set_request(user, uid, recurrenceid, "requests")
paul@142 536
paul@343 537
    def set_cancellation(self, user, uid, recurrenceid=None):
paul@343 538
paul@343 539
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 540
paul@343 541
        return self._set_request(user, uid, recurrenceid, "cancellations")
paul@142 542
paul@343 543
    def queue_request(self, user, uid, recurrenceid=None):
paul@142 544
paul@343 545
        """
paul@343 546
        Queue a request for 'user' having the given 'uid'. If the optional
paul@343 547
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 548
        or occurrence of an event.
paul@343 549
        """
paul@66 550
paul@81 551
        requests = self.get_requests(user) or []
paul@66 552
paul@343 553
        if (uid, recurrenceid) not in requests:
paul@343 554
            return self.set_request(user, uid, recurrenceid)
paul@66 555
paul@66 556
        return False
paul@66 557
paul@343 558
    def dequeue_request(self, user, uid, recurrenceid=None):
paul@105 559
paul@343 560
        """
paul@343 561
        Dequeue a request for 'user' having the given 'uid'. If the optional
paul@343 562
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 563
        or occurrence of an event.
paul@343 564
        """
paul@105 565
paul@105 566
        requests = self.get_requests(user) or []
paul@105 567
paul@105 568
        try:
paul@343 569
            requests.remove((uid, recurrenceid))
paul@105 570
            self.set_requests(user, requests)
paul@105 571
        except ValueError:
paul@105 572
            return False
paul@105 573
        else:
paul@105 574
            return True
paul@105 575
paul@343 576
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 577
paul@343 578
        """
paul@343 579
        Queue an event for cancellation for 'user' having the given 'uid'. If
paul@343 580
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 581
        occurrence of an event is cancelled.
paul@343 582
        """
paul@142 583
paul@142 584
        cancellations = self.get_cancellations(user) or []
paul@142 585
paul@343 586
        if (uid, recurrenceid) not in cancellations:
paul@343 587
            return self.set_cancellation(user, uid, recurrenceid)
paul@142 588
paul@142 589
        return False
paul@142 590
paul@30 591
class FilePublisher(FileBase):
paul@30 592
paul@30 593
    "A publisher of objects."
paul@30 594
paul@597 595
    def __init__(self, store_dir=None):
paul@597 596
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 597
paul@30 598
    def set_freebusy(self, user, freebusy):
paul@30 599
paul@30 600
        "For the given 'user', set 'freebusy' details."
paul@30 601
paul@52 602
        filename = self.get_object_in_store(user, "freebusy")
paul@30 603
        if not filename:
paul@30 604
            return False
paul@30 605
paul@30 606
        record = []
paul@30 607
        rwrite = record.append
paul@30 608
paul@30 609
        rwrite(("ORGANIZER", {}, user))
paul@30 610
        rwrite(("UID", {}, user))
paul@30 611
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 612
paul@458 613
        for fb in freebusy:
paul@458 614
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 615
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 616
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 617
paul@395 618
        f = open(filename, "wb")
paul@30 619
        try:
paul@30 620
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 621
        finally:
paul@30 622
            f.close()
paul@103 623
            fix_permissions(filename)
paul@30 624
paul@30 625
        return True
paul@30 626
paul@2 627
# vim: tabstop=4 expandtab shiftwidth=4