imip-agent

Annotated imip_store.py

672:70fb2f784339
2015-09-01 Paul Boddie Added updates to the free/busy providers when objects are handled, with new events being added to the providers and cancelled events being removed. Changed the order of object storage and free/busy updates in handlers so that incomplete objects (such as cancellation requests) do not affect inspection of any previously-stored objects. Added parameterisation of the freebusy tool for use in test scripts.
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@672 354
    def _get_freebusy_providers(self, user):
paul@672 355
paul@672 356
        """
paul@672 357
        Return the free/busy providers for the given 'user'.
paul@672 358
paul@672 359
        This function returns any stored datetime and a list of providers as a
paul@672 360
        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
paul@672 361
        """
paul@672 362
paul@672 363
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 364
        if not filename or not exists(filename):
paul@672 365
            return None
paul@672 366
paul@672 367
        # Attempt to read providers, with a declaration of the datetime
paul@672 368
        # from which such providers are considered as still being active.
paul@672 369
paul@672 370
        t = self._get_table(user, filename, [(1, None)])
paul@672 371
        try:
paul@672 372
            dt_string = t[0][0]
paul@672 373
        except IndexError:
paul@672 374
            return None
paul@672 375
paul@672 376
        return dt_string, t[1:]
paul@672 377
paul@652 378
    def get_freebusy_providers(self, user, dt=None):
paul@652 379
paul@652 380
        """
paul@652 381
        Return a set of uncancelled events of the form (uid, recurrenceid)
paul@652 382
        providing free/busy details beyond the given datetime 'dt'.
paul@654 383
paul@654 384
        If 'dt' is not specified, all events previously found to provide
paul@654 385
        details will be returned. Otherwise, if 'dt' is earlier than the
paul@654 386
        datetime recorded for the known providers, None is returned, indicating
paul@654 387
        that the list of providers must be recomputed.
paul@672 388
paul@672 389
        This function returns a list of (uid, recurrenceid) tuples upon success.
paul@652 390
        """
paul@652 391
paul@672 392
        t = self._get_freebusy_providers(user)
paul@672 393
        if not t:
paul@672 394
            return None
paul@672 395
paul@672 396
        dt_string, t = t
paul@672 397
paul@672 398
        # If the requested datetime is earlier than the stated datetime, the
paul@672 399
        # providers will need to be recomputed.
paul@672 400
paul@672 401
        if dt:
paul@672 402
            providers_dt = get_datetime(dt_string)
paul@672 403
            if not providers_dt or providers_dt > dt:
paul@672 404
                return None
paul@672 405
paul@672 406
        # Otherwise, return the providers.
paul@672 407
paul@672 408
        return t[1:]
paul@672 409
paul@672 410
    def _set_freebusy_providers(self, user, dt_string, t):
paul@672 411
paul@672 412
        "Set the given provider timestamp 'dt_string' and table 't'."
paul@672 413
paul@652 414
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 415
        if not filename:
paul@672 416
            return False
paul@652 417
paul@672 418
        t.insert(0, (dt_string,))
paul@672 419
        self._set_table(user, filename, t, [(1, "")])
paul@672 420
        return True
paul@652 421
paul@654 422
    def set_freebusy_providers(self, user, dt, providers):
paul@654 423
paul@654 424
        """
paul@654 425
        Define the uncancelled events providing free/busy details beyond the
paul@654 426
        given datetime 'dt'.
paul@654 427
        """
paul@654 428
paul@672 429
        t = []
paul@654 430
paul@654 431
        for obj in providers:
paul@672 432
            t.append((obj.get_uid(), obj.get_recurrenceid()))
paul@672 433
paul@672 434
        return self._set_freebusy_providers(user, format_datetime(dt), t)
paul@654 435
paul@672 436
    def append_freebusy_provider(self, user, provider):
paul@672 437
paul@672 438
        "For the given 'user', append the free/busy 'provider'."
paul@672 439
paul@672 440
        t = self._get_freebusy_providers(user)
paul@672 441
        if not t:
paul@654 442
            return False
paul@654 443
paul@672 444
        dt_string, t = t
paul@672 445
        t.append((provider.get_uid(), provider.get_recurrenceid()))
paul@672 446
paul@672 447
        return self._set_freebusy_providers(user, dt_string, t)
paul@672 448
paul@672 449
    def remove_freebusy_provider(self, user, provider):
paul@672 450
paul@672 451
        "For the given 'user', remove the free/busy 'provider'."
paul@672 452
paul@672 453
        t = self._get_freebusy_providers(user)
paul@672 454
        if not t:
paul@672 455
            return False
paul@672 456
paul@672 457
        dt_string, t = t
paul@672 458
        try:
paul@672 459
            t.remove((provider.get_uid(), provider.get_recurrenceid()))
paul@672 460
        except ValueError:
paul@672 461
            return False
paul@672 462
paul@672 463
        return self._set_freebusy_providers(user, dt_string, t)
paul@654 464
paul@648 465
    # Free/busy period access.
paul@648 466
paul@15 467
    def get_freebusy(self, user):
paul@15 468
paul@15 469
        "Get free/busy details for the given 'user'."
paul@15 470
paul@52 471
        filename = self.get_object_in_store(user, "freebusy")
paul@15 472
        if not filename or not exists(filename):
paul@167 473
            return []
paul@112 474
        else:
paul@458 475
            return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))
paul@2 476
paul@112 477
    def get_freebusy_for_other(self, user, other):
paul@112 478
paul@112 479
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 480
paul@112 481
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@167 482
        if not filename or not exists(filename):
paul@167 483
            return []
paul@112 484
        else:
paul@458 485
            return map(lambda t: FreeBusyPeriod(*t), self._get_table(user, filename, [(4, None)]))
paul@2 486
paul@15 487
    def set_freebusy(self, user, freebusy):
paul@15 488
paul@15 489
        "For the given 'user', set 'freebusy' details."
paul@15 490
paul@52 491
        filename = self.get_object_in_store(user, "freebusy")
paul@15 492
        if not filename:
paul@15 493
            return False
paul@15 494
paul@563 495
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@15 496
        return True
paul@15 497
paul@110 498
    def set_freebusy_for_other(self, user, freebusy, other):
paul@110 499
paul@110 500
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 501
paul@110 502
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 503
        if not filename:
paul@110 504
            return False
paul@110 505
paul@563 506
        self._set_table(user, filename, map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
paul@112 507
        return True
paul@112 508
paul@648 509
    # Object status details access.
paul@648 510
paul@142 511
    def _get_requests(self, user, queue):
paul@66 512
paul@142 513
        "Get requests for the given 'user' from the given 'queue'."
paul@66 514
paul@142 515
        filename = self.get_object_in_store(user, queue)
paul@81 516
        if not filename or not exists(filename):
paul@66 517
            return None
paul@66 518
paul@343 519
        return self._get_table(user, filename, [(1, None)])
paul@66 520
paul@142 521
    def get_requests(self, user):
paul@142 522
paul@142 523
        "Get requests for the given 'user'."
paul@142 524
paul@142 525
        return self._get_requests(user, "requests")
paul@142 526
paul@142 527
    def get_cancellations(self, user):
paul@142 528
paul@142 529
        "Get cancellations for the given 'user'."
paul@55 530
paul@142 531
        return self._get_requests(user, "cancellations")
paul@142 532
paul@142 533
    def _set_requests(self, user, requests, queue):
paul@66 534
paul@142 535
        """
paul@142 536
        For the given 'user', set the list of queued 'requests' in the given
paul@142 537
        'queue'.
paul@142 538
        """
paul@142 539
paul@142 540
        filename = self.get_object_in_store(user, queue)
paul@66 541
        if not filename:
paul@66 542
            return False
paul@66 543
paul@303 544
        self.acquire_lock(user)
paul@66 545
        try:
paul@303 546
            f = open(filename, "w")
paul@303 547
            try:
paul@303 548
                for request in requests:
paul@343 549
                    print >>f, "\t".join([value or "" for value in request])
paul@303 550
            finally:
paul@303 551
                f.close()
paul@303 552
                fix_permissions(filename)
paul@66 553
        finally:
paul@303 554
            self.release_lock(user)
paul@66 555
paul@66 556
        return True
paul@66 557
paul@142 558
    def set_requests(self, user, requests):
paul@142 559
paul@142 560
        "For the given 'user', set the list of queued 'requests'."
paul@142 561
paul@142 562
        return self._set_requests(user, requests, "requests")
paul@142 563
paul@142 564
    def set_cancellations(self, user, cancellations):
paul@66 565
paul@142 566
        "For the given 'user', set the list of queued 'cancellations'."
paul@142 567
paul@142 568
        return self._set_requests(user, cancellations, "cancellations")
paul@55 569
paul@343 570
    def _set_request(self, user, uid, recurrenceid, queue):
paul@142 571
paul@343 572
        """
paul@343 573
        For the given 'user', set the queued 'uid' and 'recurrenceid' in the
paul@343 574
        given 'queue'.
paul@343 575
        """
paul@142 576
paul@142 577
        filename = self.get_object_in_store(user, queue)
paul@55 578
        if not filename:
paul@55 579
            return False
paul@55 580
paul@303 581
        self.acquire_lock(user)
paul@55 582
        try:
paul@303 583
            f = open(filename, "a")
paul@303 584
            try:
paul@343 585
                print >>f, "\t".join([uid, recurrenceid or ""])
paul@303 586
            finally:
paul@303 587
                f.close()
paul@303 588
                fix_permissions(filename)
paul@55 589
        finally:
paul@303 590
            self.release_lock(user)
paul@55 591
paul@55 592
        return True
paul@55 593
paul@343 594
    def set_request(self, user, uid, recurrenceid=None):
paul@142 595
paul@343 596
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 597
paul@343 598
        return self._set_request(user, uid, recurrenceid, "requests")
paul@142 599
paul@343 600
    def set_cancellation(self, user, uid, recurrenceid=None):
paul@343 601
paul@343 602
        "For the given 'user', set the queued 'uid' and 'recurrenceid'."
paul@142 603
paul@343 604
        return self._set_request(user, uid, recurrenceid, "cancellations")
paul@142 605
paul@343 606
    def queue_request(self, user, uid, recurrenceid=None):
paul@142 607
paul@343 608
        """
paul@343 609
        Queue a request for 'user' having the given 'uid'. If the optional
paul@343 610
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 611
        or occurrence of an event.
paul@343 612
        """
paul@66 613
paul@81 614
        requests = self.get_requests(user) or []
paul@66 615
paul@343 616
        if (uid, recurrenceid) not in requests:
paul@343 617
            return self.set_request(user, uid, recurrenceid)
paul@66 618
paul@66 619
        return False
paul@66 620
paul@343 621
    def dequeue_request(self, user, uid, recurrenceid=None):
paul@105 622
paul@343 623
        """
paul@343 624
        Dequeue a request for 'user' having the given 'uid'. If the optional
paul@343 625
        'recurrenceid' is specified, the request refers to a specific instance
paul@343 626
        or occurrence of an event.
paul@343 627
        """
paul@105 628
paul@105 629
        requests = self.get_requests(user) or []
paul@105 630
paul@105 631
        try:
paul@343 632
            requests.remove((uid, recurrenceid))
paul@105 633
            self.set_requests(user, requests)
paul@105 634
        except ValueError:
paul@105 635
            return False
paul@105 636
        else:
paul@105 637
            return True
paul@105 638
paul@343 639
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 640
paul@343 641
        """
paul@343 642
        Queue an event for cancellation for 'user' having the given 'uid'. If
paul@343 643
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 644
        occurrence of an event is cancelled.
paul@343 645
        """
paul@142 646
paul@142 647
        cancellations = self.get_cancellations(user) or []
paul@142 648
paul@343 649
        if (uid, recurrenceid) not in cancellations:
paul@343 650
            return self.set_cancellation(user, uid, recurrenceid)
paul@142 651
paul@142 652
        return False
paul@142 653
paul@30 654
class FilePublisher(FileBase):
paul@30 655
paul@30 656
    "A publisher of objects."
paul@30 657
paul@597 658
    def __init__(self, store_dir=None):
paul@597 659
        FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@30 660
paul@30 661
    def set_freebusy(self, user, freebusy):
paul@30 662
paul@30 663
        "For the given 'user', set 'freebusy' details."
paul@30 664
paul@52 665
        filename = self.get_object_in_store(user, "freebusy")
paul@30 666
        if not filename:
paul@30 667
            return False
paul@30 668
paul@30 669
        record = []
paul@30 670
        rwrite = record.append
paul@30 671
paul@30 672
        rwrite(("ORGANIZER", {}, user))
paul@30 673
        rwrite(("UID", {}, user))
paul@30 674
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 675
paul@458 676
        for fb in freebusy:
paul@458 677
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 678
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 679
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 680
paul@395 681
        f = open(filename, "wb")
paul@30 682
        try:
paul@30 683
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 684
        finally:
paul@30 685
            f.close()
paul@103 686
            fix_permissions(filename)
paul@30 687
paul@30 688
        return True
paul@30 689
paul@2 690
# vim: tabstop=4 expandtab shiftwidth=4