imip-agent

Annotated imiptools/stores/file.py

1359:8cb064fcd9f1
2017-10-22 Paul Boddie Reworked various aspects of the recurrence computation implementation, removing explicit sort operations and changing day selection to produce results in order.
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@1210 6
Copyright (C) 2014, 2015, 2016, 2017 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@1340 22
from imiptools.stores.common import StoreBase, PublisherBase, JournalBase, \
paul@1340 23
                                    StoreInitialisationError
paul@1069 24
paul@30 25
from datetime import datetime
paul@1210 26
from imiptools.config import settings
paul@1232 27
from imiptools.data import Object, make_calendar, parse_object, to_stream
paul@740 28
from imiptools.dates import format_datetime, get_datetime, to_timezone
paul@147 29
from imiptools.filesys import fix_permissions, FileBase
paul@1236 30
paul@1236 31
from imiptools.freebusy import FreeBusyCollection, \
paul@1234 32
                               FreeBusyGroupCollection, \
paul@1236 33
                               FreeBusyOffersCollection, \
paul@1236 34
                               period_from_tuple, \
paul@1236 35
                               period_to_tuple
paul@1236 36
paul@1236 37
from imiptools.text import FileTable, FileTableDict, FileTableSingle, \
paul@1236 38
                           have_table
paul@1236 39
paul@808 40
from os.path import isdir, isfile, join
paul@343 41
from os import listdir, remove, rmdir
paul@15 42
paul@1230 43
# Obtain defaults from the settings.
paul@1230 44
paul@1210 45
STORE_DIR = settings["STORE_DIR"]
paul@1210 46
PUBLISH_DIR = settings["PUBLISH_DIR"]
paul@1210 47
JOURNAL_DIR = settings["JOURNAL_DIR"]
paul@1210 48
paul@1232 49
# Store classes.
paul@1232 50
paul@1039 51
class FileStoreBase(FileBase):
paul@50 52
paul@1236 53
    "A file store supporting user-specific locking."
paul@147 54
paul@303 55
    def acquire_lock(self, user, timeout=None):
paul@303 56
        FileBase.acquire_lock(self, timeout, user)
paul@303 57
paul@303 58
    def release_lock(self, user):
paul@303 59
        FileBase.release_lock(self, user)
paul@303 60
paul@1088 61
class Store(FileStoreBase, StoreBase):
paul@1039 62
paul@1039 63
    "A file store of tabular free/busy data and objects."
paul@1039 64
paul@1039 65
    def __init__(self, store_dir=None):
paul@1340 66
        try:
paul@1340 67
            FileBase.__init__(self, store_dir or STORE_DIR)
paul@1340 68
        except OSError, exc:
paul@1340 69
            raise StoreInitialisationError, exc
paul@1039 70
paul@648 71
    # Store object access.
paul@648 72
paul@329 73
    def _get_object(self, user, filename):
paul@329 74
paul@329 75
        """
paul@329 76
        Return the parsed object for the given 'user' having the given
paul@329 77
        'filename'.
paul@329 78
        """
paul@329 79
paul@329 80
        self.acquire_lock(user)
paul@329 81
        try:
paul@329 82
            f = open(filename, "rb")
paul@329 83
            try:
paul@1232 84
                return Object(parse_object(f, "utf-8"))
paul@329 85
            finally:
paul@329 86
                f.close()
paul@329 87
        finally:
paul@329 88
            self.release_lock(user)
paul@329 89
paul@329 90
    def _set_object(self, user, filename, node):
paul@329 91
paul@329 92
        """
paul@329 93
        Set an object for the given 'user' having the given 'filename', using
paul@329 94
        'node' to define the object.
paul@329 95
        """
paul@329 96
paul@329 97
        self.acquire_lock(user)
paul@329 98
        try:
paul@329 99
            f = open(filename, "wb")
paul@329 100
            try:
paul@329 101
                to_stream(f, node)
paul@329 102
            finally:
paul@329 103
                f.close()
paul@329 104
                fix_permissions(filename)
paul@329 105
        finally:
paul@329 106
            self.release_lock(user)
paul@329 107
paul@329 108
        return True
paul@329 109
paul@329 110
    def _remove_object(self, filename):
paul@329 111
paul@329 112
        "Remove the object with the given 'filename'."
paul@329 113
paul@329 114
        try:
paul@329 115
            remove(filename)
paul@329 116
        except OSError:
paul@329 117
            return False
paul@329 118
paul@329 119
        return True
paul@329 120
paul@343 121
    def _remove_collection(self, filename):
paul@343 122
paul@343 123
        "Remove the collection with the given 'filename'."
paul@343 124
paul@343 125
        try:
paul@343 126
            rmdir(filename)
paul@343 127
        except OSError:
paul@343 128
            return False
paul@343 129
paul@343 130
        return True
paul@343 131
paul@670 132
    # User discovery.
paul@670 133
paul@670 134
    def get_users(self):
paul@670 135
paul@670 136
        "Return a list of users."
paul@670 137
paul@670 138
        return listdir(self.store_dir)
paul@670 139
paul@648 140
    # Event and event metadata access.
paul@648 141
paul@119 142
    def get_events(self, user):
paul@119 143
paul@119 144
        "Return a list of event identifiers."
paul@119 145
paul@138 146
        filename = self.get_object_in_store(user, "objects")
paul@808 147
        if not filename or not isdir(filename):
paul@1142 148
            return []
paul@119 149
paul@119 150
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 151
paul@1142 152
    def get_cancelled_events(self, user):
paul@648 153
paul@1142 154
        "Return a list of event identifiers for cancelled events."
paul@648 155
paul@1142 156
        filename = self.get_object_in_store(user, "cancellations", "objects")
paul@1142 157
        if not filename or not isdir(filename):
paul@1142 158
            return []
paul@648 159
paul@1142 160
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@648 161
paul@858 162
    def get_event(self, user, uid, recurrenceid=None, dirname=None):
paul@343 163
paul@343 164
        """
paul@343 165
        Get the event for the given 'user' with the given 'uid'. If
paul@343 166
        the optional 'recurrenceid' is specified, a specific instance or
paul@343 167
        occurrence of an event is returned.
paul@343 168
        """
paul@343 169
paul@858 170
        filename = self.get_event_filename(user, uid, recurrenceid, dirname)
paul@808 171
        if not filename or not isfile(filename):
paul@694 172
            return None
paul@694 173
paul@694 174
        return filename and self._get_object(user, filename)
paul@694 175
paul@1324 176
    def set_parent_event(self, user, uid, node):
paul@50 177
paul@50 178
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 179
paul@138 180
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 181
        if not filename:
paul@50 182
            return False
paul@50 183
paul@329 184
        return self._set_object(user, filename, node)
paul@15 185
paul@1068 186
    def remove_parent_event(self, user, uid):
paul@1068 187
paul@1068 188
        "Remove the parent event for 'user' having the given 'uid'."
paul@369 189
paul@234 190
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 191
        if not filename:
paul@234 192
            return False
paul@234 193
paul@329 194
        return self._remove_object(filename)
paul@234 195
paul@334 196
    def get_recurrences(self, user, uid):
paul@334 197
paul@334 198
        """
paul@334 199
        Get additional event instances for an event of the given 'user' with the
paul@694 200
        indicated 'uid'. Both active and cancelled recurrences are returned.
paul@694 201
        """
paul@694 202
paul@694 203
        return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
paul@694 204
paul@694 205
    def get_active_recurrences(self, user, uid):
paul@694 206
paul@694 207
        """
paul@694 208
        Get additional event instances for an event of the given 'user' with the
paul@694 209
        indicated 'uid'. Cancelled recurrences are not returned.
paul@334 210
        """
paul@334 211
paul@334 212
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@808 213
        if not filename or not isdir(filename):
paul@347 214
            return []
paul@334 215
paul@334 216
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 217
paul@694 218
    def get_cancelled_recurrences(self, user, uid):
paul@694 219
paul@694 220
        """
paul@694 221
        Get additional event instances for an event of the given 'user' with the
paul@694 222
        indicated 'uid'. Only cancelled recurrences are returned.
paul@694 223
        """
paul@694 224
paul@782 225
        filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
paul@808 226
        if not filename or not isdir(filename):
paul@694 227
            return []
paul@694 228
paul@694 229
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@694 230
paul@334 231
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 232
paul@334 233
        """
paul@334 234
        For the event of the given 'user' with the given 'uid', return the
paul@334 235
        specific recurrence indicated by the 'recurrenceid'.
paul@334 236
        """
paul@334 237
paul@694 238
        filename = self.get_recurrence_filename(user, uid, recurrenceid)
paul@808 239
        if not filename or not isfile(filename):
paul@334 240
            return None
paul@334 241
paul@694 242
        return filename and self._get_object(user, filename)
paul@334 243
paul@334 244
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 245
paul@334 246
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 247
paul@334 248
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 249
        if not filename:
paul@334 250
            return False
paul@334 251
paul@334 252
        return self._set_object(user, filename, node)
paul@334 253
paul@334 254
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 255
paul@378 256
        """
paul@378 257
        Remove a special recurrence from an event stored by 'user' having the
paul@378 258
        given 'uid' and 'recurrenceid'.
paul@378 259
        """
paul@334 260
paul@378 261
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 262
        if not filename:
paul@334 263
            return False
paul@334 264
paul@334 265
        return self._remove_object(filename)
paul@334 266
paul@1068 267
    def remove_recurrence_collection(self, user, uid):
paul@1068 268
paul@1068 269
        """
paul@1068 270
        Remove the collection of recurrences stored by 'user' having the given
paul@1068 271
        'uid'.
paul@1068 272
        """
paul@1068 273
paul@378 274
        recurrences = self.get_object_in_store(user, "recurrences", uid)
paul@378 275
        if recurrences:
paul@378 276
            return self._remove_collection(recurrences)
paul@378 277
paul@378 278
        return True
paul@378 279
paul@1142 280
    # Event filename computation.
paul@1142 281
paul@1142 282
    def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
paul@1142 283
paul@1142 284
        """
paul@1142 285
        Get the filename providing the event for the given 'user' with the given
paul@1142 286
        'uid'. If the optional 'recurrenceid' is specified, a specific instance
paul@1142 287
        or occurrence of an event is returned.
paul@1142 288
paul@1142 289
        Where 'dirname' is specified, the given directory name is used as the
paul@1142 290
        base of the location within which any filename will reside.
paul@1142 291
        """
paul@1142 292
paul@1142 293
        if recurrenceid:
paul@1142 294
            return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
paul@1142 295
        else:
paul@1324 296
            return self.get_parent_event_filename(user, uid, dirname, username)
paul@1142 297
paul@1142 298
    def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
paul@1142 299
paul@1142 300
        """
paul@1142 301
        For the event of the given 'user' with the given 'uid', return the
paul@1142 302
        filename providing the recurrence with the given 'recurrenceid'.
paul@1142 303
paul@1142 304
        Where 'dirname' is specified, the given directory name is used as the
paul@1142 305
        base of the location within which any filename will reside.
paul@1142 306
paul@1142 307
        Where 'username' is specified, the event details will reside in a file
paul@1142 308
        bearing that name within a directory having 'uid' as its name.
paul@1142 309
        """
paul@1142 310
paul@1142 311
        return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
paul@1142 312
paul@1324 313
    def get_parent_event_filename(self, user, uid, dirname=None, username=None):
paul@1142 314
paul@1142 315
        """
paul@1142 316
        Get the filename providing the event for the given 'user' with the given
paul@1142 317
        'uid'. 
paul@1142 318
paul@1142 319
        Where 'dirname' is specified, the given directory name is used as the
paul@1142 320
        base of the location within which any filename will reside.
paul@1142 321
paul@1142 322
        Where 'username' is specified, the event details will reside in a file
paul@1142 323
        bearing that name within a directory having 'uid' as its name.
paul@1142 324
        """
paul@1142 325
paul@1142 326
        return self.get_object_in_store(user, dirname, "objects", uid, username)
paul@1142 327
paul@652 328
    # Free/busy period providers, upon extension of the free/busy records.
paul@652 329
paul@672 330
    def _get_freebusy_providers(self, user):
paul@672 331
paul@672 332
        """
paul@672 333
        Return the free/busy providers for the given 'user'.
paul@672 334
paul@672 335
        This function returns any stored datetime and a list of providers as a
paul@672 336
        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
paul@672 337
        """
paul@672 338
paul@672 339
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@1236 340
        if not filename:
paul@672 341
            return None
paul@672 342
paul@672 343
        # Attempt to read providers, with a declaration of the datetime
paul@672 344
        # from which such providers are considered as still being active.
paul@672 345
paul@1236 346
        t = self._get_freebusy_providers_table(filename)
paul@1236 347
        header = t.get_header_values()
paul@1236 348
        if not header:
paul@672 349
            return None
paul@672 350
paul@1236 351
        return header[0], t
paul@1236 352
paul@1236 353
    def _get_freebusy_providers_table(self, filename):
paul@1236 354
paul@1236 355
        "Return a file-based table for storing providers in 'filename'."
paul@672 356
paul@1236 357
        return FileTable(filename,
paul@1236 358
                         in_defaults=[(1, None)],
paul@1236 359
                         out_defaults=[(1, "")],
paul@1236 360
                         headers=1)
paul@672 361
paul@1236 362
    def _set_freebusy_providers(self, user, dt_string, providers):
paul@1236 363
paul@1236 364
        "Set the given provider timestamp 'dt_string' and 'providers'."
paul@672 365
paul@652 366
        filename = self.get_object_in_store(user, "freebusy-providers")
paul@672 367
        if not filename:
paul@672 368
            return False
paul@652 369
paul@1236 370
        self.acquire_lock(user)
paul@1236 371
        try:
paul@1236 372
            if not have_table(providers, filename):
paul@1236 373
                pr = self._get_freebusy_providers_table(filename)
paul@1236 374
                pr.replaceall(providers)
paul@1236 375
                providers = pr
paul@1236 376
            providers.set_header_values([dt_string])
paul@1236 377
            providers.close()
paul@1236 378
        finally:
paul@1236 379
            self.release_lock(user)
paul@672 380
        return True
paul@652 381
paul@648 382
    # Free/busy period access.
paul@648 383
paul@1236 384
    def get_freebusy(self, user, name=None, mutable=False):
paul@15 385
paul@15 386
        "Get free/busy details for the given 'user'."
paul@15 387
paul@702 388
        filename = self.get_object_in_store(user, name or "freebusy")
paul@1062 389
paul@1236 390
        if not filename:
paul@1236 391
            return []
paul@702 392
paul@1236 393
        return self._get_freebusy(filename, mutable, FreeBusyCollection)
paul@1071 394
paul@1236 395
    def get_freebusy_for_other(self, user, other, mutable=False, collection=None):
paul@112 396
paul@112 397
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 398
paul@112 399
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@1062 400
paul@1236 401
        if not filename:
paul@1236 402
            return []
paul@1236 403
paul@1236 404
        return self._get_freebusy(filename, mutable, collection or FreeBusyCollection)
paul@1236 405
paul@1236 406
    def _get_freebusy(self, filename, mutable=False, collection=None):
paul@1236 407
paul@1236 408
        """
paul@1236 409
        Return a free/busy collection for 'filename' with the given 'mutable'
paul@1236 410
        condition, employing the specified 'collection' class.
paul@1236 411
        """
paul@702 412
paul@1192 413
        collection = collection or FreeBusyCollection
paul@1236 414
paul@1236 415
        periods = FileTable(filename, mutable=mutable,
paul@1236 416
                            in_converter=period_from_tuple(collection.period_class),
paul@1236 417
                            out_converter=period_to_tuple)
paul@1236 418
paul@1236 419
        return collection(periods, mutable=mutable)
paul@1071 420
paul@1064 421
    def set_freebusy(self, user, freebusy, name=None):
paul@15 422
paul@15 423
        "For the given 'user', set 'freebusy' details."
paul@15 424
paul@702 425
        filename = self.get_object_in_store(user, name or "freebusy")
paul@15 426
        if not filename:
paul@15 427
            return False
paul@15 428
paul@1236 429
        return self._set_freebusy(user, freebusy, filename)
paul@15 430
paul@1236 431
    def set_freebusy_for_other(self, user, freebusy, other, collection=None):
paul@110 432
paul@110 433
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 434
paul@110 435
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 436
        if not filename:
paul@110 437
            return False
paul@110 438
paul@1236 439
        return self._set_freebusy(user, freebusy, filename, collection)
paul@1236 440
paul@1236 441
    def _set_freebusy(self, user, freebusy, filename, collection=None):
paul@1236 442
paul@1236 443
        "For the given 'user', set 'freebusy' details for the given 'filename'."
paul@1236 444
paul@1236 445
        # Copy to the specified table if different from that given.
paul@1236 446
paul@1236 447
        self.acquire_lock(user)
paul@1236 448
        try:
paul@1236 449
            if not have_table(freebusy, filename):
paul@1236 450
                fbc = self._get_freebusy(filename, True, collection)
paul@1236 451
                fbc += freebusy
paul@1236 452
                freebusy = fbc
paul@1236 453
            freebusy.close()
paul@1236 454
        finally:
paul@1236 455
            self.release_lock(user)
paul@1236 456
paul@112 457
        return True
paul@112 458
paul@1142 459
    def get_freebusy_others(self, user):
paul@1142 460
paul@1142 461
        """
paul@1142 462
        For the given 'user', return a list of other users for whom free/busy
paul@1142 463
        information is retained.
paul@1142 464
        """
paul@1142 465
paul@1142 466
        filename = self.get_object_in_store(user, "freebusy-other")
paul@1142 467
paul@1142 468
        if not filename or not isdir(filename):
paul@1142 469
            return []
paul@1142 470
paul@1142 471
        return listdir(filename)
paul@1142 472
paul@710 473
    # Tentative free/busy periods related to countering.
paul@710 474
paul@1071 475
    def get_freebusy_offers(self, user, mutable=False):
paul@710 476
paul@710 477
        "Get free/busy offers for the given 'user'."
paul@710 478
paul@1236 479
        filename = self.get_object_in_store(user, "freebusy-offers")
paul@1236 480
paul@1236 481
        if not filename:
paul@1236 482
            return []
paul@1236 483
paul@710 484
        expired = []
paul@741 485
        now = to_timezone(datetime.utcnow(), "UTC")
paul@710 486
paul@710 487
        # Expire old offers and save the collection if modified.
paul@710 488
paul@730 489
        self.acquire_lock(user)
paul@710 490
        try:
paul@1236 491
            offers = self._get_freebusy(filename, True, FreeBusyOffersCollection)
paul@1236 492
            for fb in offers:
paul@710 493
                if fb.expires and get_datetime(fb.expires) <= now:
paul@1236 494
                    offers.remove(fb)
paul@710 495
            if expired:
paul@1236 496
                offers.close()
paul@710 497
        finally:
paul@730 498
            self.release_lock(user)
paul@710 499
paul@1236 500
        offers.mutable = mutable
paul@1236 501
        return offers
paul@710 502
paul@747 503
    # Requests and counter-proposals.
paul@648 504
paul@1236 505
    def get_requests(self, user, queue="requests"):
paul@66 506
paul@142 507
        "Get requests for the given 'user' from the given 'queue'."
paul@66 508
paul@142 509
        filename = self.get_object_in_store(user, queue)
paul@1236 510
        if not filename:
paul@1142 511
            return []
paul@66 512
paul@1236 513
        return FileTable(filename,
paul@1236 514
                         in_defaults=[(1, None), (2, None)],
paul@1236 515
                         out_defaults=[(1, ""), (2, "")])
paul@66 516
paul@1236 517
    def set_request(self, user, uid, recurrenceid=None, type=None):
paul@142 518
paul@1236 519
        """
paul@1236 520
        For the given 'user', set the queued 'uid' and 'recurrenceid',
paul@1236 521
        indicating a request, along with any given 'type'.
paul@1236 522
        """
paul@142 523
paul@1236 524
        requests = self.get_requests(user)
paul@1236 525
        return self.set_requests(user, [(uid, recurrenceid, type)])
paul@142 526
paul@1236 527
    def set_requests(self, user, requests, queue="requests"):
paul@66 528
paul@142 529
        """
paul@142 530
        For the given 'user', set the list of queued 'requests' in the given
paul@142 531
        'queue'.
paul@142 532
        """
paul@142 533
paul@142 534
        filename = self.get_object_in_store(user, queue)
paul@66 535
        if not filename:
paul@66 536
            return False
paul@66 537
paul@1236 538
        # Copy to the specified table if different from that given.
paul@55 539
paul@303 540
        self.acquire_lock(user)
paul@55 541
        try:
paul@1236 542
            if not have_table(requests, filename):
paul@1236 543
                req = self.get_requests(user, queue)
paul@1236 544
                req.replaceall(requests)
paul@1236 545
                requests = req
paul@1236 546
            requests.close()
paul@55 547
        finally:
paul@303 548
            self.release_lock(user)
paul@55 549
paul@55 550
        return True
paul@55 551
paul@760 552
    def get_counters(self, user, uid, recurrenceid=None):
paul@754 553
paul@754 554
        """
paul@766 555
        For the given 'user', return a list of users from whom counter-proposals
paul@766 556
        have been received for the given 'uid' and optional 'recurrenceid'.
paul@754 557
        """
paul@754 558
paul@754 559
        filename = self.get_event_filename(user, uid, recurrenceid, "counters")
paul@808 560
        if not filename or not isdir(filename):
paul@1142 561
            return []
paul@754 562
paul@766 563
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@760 564
paul@1305 565
    def get_counter_recurrences(self, user, uid):
paul@1305 566
paul@1305 567
        """
paul@1305 568
        For the given 'user', return a list of recurrence identifiers describing
paul@1305 569
        counter-proposals for the parent event with the given 'uid'.
paul@1305 570
        """
paul@1305 571
paul@1305 572
        filename = self.get_object_in_store(user, "counters", "recurrences", uid)
paul@1305 573
        if not filename or not isdir(filename):
paul@1305 574
            return []
paul@1305 575
paul@1305 576
        return listdir(filename)
paul@1305 577
paul@760 578
    def get_counter(self, user, other, uid, recurrenceid=None):
paul@105 579
paul@343 580
        """
paul@760 581
        For the given 'user', return the counter-proposal from 'other' for the
paul@760 582
        given 'uid' and optional 'recurrenceid'.
paul@760 583
        """
paul@760 584
paul@760 585
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@1090 586
        if not filename or not isfile(filename):
paul@1142 587
            return None
paul@760 588
paul@760 589
        return self._get_object(user, filename)
paul@760 590
paul@760 591
    def set_counter(self, user, other, node, uid, recurrenceid=None):
paul@760 592
paul@760 593
        """
paul@760 594
        For the given 'user', store a counter-proposal received from 'other' the
paul@760 595
        given 'node' representing that proposal for the given 'uid' and
paul@760 596
        'recurrenceid'.
paul@760 597
        """
paul@760 598
paul@760 599
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@760 600
        if not filename:
paul@760 601
            return False
paul@760 602
paul@760 603
        return self._set_object(user, filename, node)
paul@760 604
paul@1306 605
    def remove_counters(self, user, uid, recurrenceid=None, attendee=None):
paul@760 606
paul@760 607
        """
paul@760 608
        For the given 'user', remove all counter-proposals associated with the
paul@1306 609
        given 'uid' and 'recurrenceid'. If a parent event is specified, all
paul@1306 610
        recurrence counter-proposals will be removed. If 'attendee' is
paul@1306 611
        specified, only objects provided by this attendee will be removed.
paul@1306 612
        """
paul@1306 613
paul@1306 614
        self._remove_counters(user, uid, recurrenceid, attendee)
paul@1306 615
paul@1306 616
        if not recurrenceid:
paul@1306 617
            for recurrenceid in self.get_counter_recurrences(user, uid):
paul@1306 618
                self._remove_counters(user, uid, recurrenceid, attendee)
paul@1306 619
paul@1306 620
    def _remove_counters(self, user, uid, recurrenceid=None, attendee=None):
paul@1306 621
paul@1306 622
        """
paul@1306 623
        For the given 'user', remove all counter-proposals associated with the
paul@1306 624
        given 'uid' and 'recurrenceid'. If 'attendee' is specified, only objects
paul@1306 625
        provided by this attendee will be removed.
paul@343 626
        """
paul@105 627
paul@747 628
        filename = self.get_event_filename(user, uid, recurrenceid, "counters")
paul@808 629
        if not filename or not isdir(filename):
paul@747 630
            return False
paul@747 631
paul@760 632
        removed = False
paul@747 633
paul@760 634
        for other in listdir(filename):
paul@1306 635
            if not attendee or other == attendee:
paul@1306 636
                counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@1306 637
                removed = removed or self._remove_object(counter_filename)
paul@1306 638
paul@1306 639
        if not listdir(filename):
paul@1306 640
            self._remove_collection(filename)
paul@760 641
paul@760 642
        return removed
paul@760 643
paul@760 644
    def remove_counter(self, user, other, uid, recurrenceid=None):
paul@105 645
paul@747 646
        """
paul@760 647
        For the given 'user', remove any counter-proposal from 'other'
paul@760 648
        associated with the given 'uid' and 'recurrenceid'.
paul@747 649
        """
paul@747 650
paul@760 651
        filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
paul@808 652
        if not filename or not isfile(filename):
paul@105 653
            return False
paul@747 654
paul@747 655
        return self._remove_object(filename)
paul@747 656
paul@747 657
    # Event cancellation.
paul@105 658
paul@343 659
    def cancel_event(self, user, uid, recurrenceid=None):
paul@142 660
paul@343 661
        """
paul@694 662
        Cancel an event for 'user' having the given 'uid'. If the optional
paul@694 663
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@694 664
        event is cancelled.
paul@343 665
        """
paul@142 666
paul@694 667
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@694 668
        cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@142 669
paul@808 670
        if filename and cancelled_filename and isfile(filename):
paul@694 671
            return self.move_object(filename, cancelled_filename)
paul@142 672
paul@142 673
        return False
paul@142 674
paul@863 675
    def uncancel_event(self, user, uid, recurrenceid=None):
paul@863 676
paul@863 677
        """
paul@863 678
        Uncancel an event for 'user' having the given 'uid'. If the optional
paul@863 679
        'recurrenceid' is specified, a specific instance or occurrence of an
paul@863 680
        event is uncancelled.
paul@863 681
        """
paul@863 682
paul@863 683
        filename = self.get_event_filename(user, uid, recurrenceid)
paul@863 684
        cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@863 685
paul@863 686
        if filename and cancelled_filename and isfile(cancelled_filename):
paul@863 687
            return self.move_object(cancelled_filename, filename)
paul@863 688
paul@863 689
        return False
paul@863 690
paul@796 691
    def remove_cancellation(self, user, uid, recurrenceid=None):
paul@796 692
paul@796 693
        """
paul@796 694
        Remove a cancellation for 'user' for the event having the given 'uid'.
paul@796 695
        If the optional 'recurrenceid' is specified, a specific instance or
paul@796 696
        occurrence of an event is affected.
paul@796 697
        """
paul@796 698
paul@796 699
        # Remove any parent event cancellation or a specific recurrence
paul@796 700
        # cancellation if indicated.
paul@796 701
paul@796 702
        filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
paul@796 703
paul@808 704
        if filename and isfile(filename):
paul@796 705
            return self._remove_object(filename)
paul@796 706
paul@796 707
        return False
paul@796 708
paul@1088 709
class Publisher(FileBase, PublisherBase):
paul@30 710
paul@30 711
    "A publisher of objects."
paul@30 712
paul@597 713
    def __init__(self, store_dir=None):
paul@1340 714
        try:
paul@1340 715
            FileBase.__init__(self, store_dir or PUBLISH_DIR)
paul@1340 716
        except OSError, exc:
paul@1340 717
            raise StoreInitialisationError, exc
paul@30 718
paul@30 719
    def set_freebusy(self, user, freebusy):
paul@30 720
paul@30 721
        "For the given 'user', set 'freebusy' details."
paul@30 722
paul@52 723
        filename = self.get_object_in_store(user, "freebusy")
paul@30 724
        if not filename:
paul@30 725
            return False
paul@30 726
paul@30 727
        record = []
paul@30 728
        rwrite = record.append
paul@30 729
paul@30 730
        rwrite(("ORGANIZER", {}, user))
paul@30 731
        rwrite(("UID", {}, user))
paul@30 732
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 733
paul@458 734
        for fb in freebusy:
paul@458 735
            if not fb.transp or fb.transp == "OPAQUE":
paul@529 736
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
paul@563 737
                    map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
paul@30 738
paul@395 739
        f = open(filename, "wb")
paul@30 740
        try:
paul@30 741
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 742
        finally:
paul@30 743
            f.close()
paul@103 744
            fix_permissions(filename)
paul@30 745
paul@30 746
        return True
paul@30 747
paul@1192 748
class Journal(Store, JournalBase):
paul@1039 749
paul@1039 750
    "A journal system to support quotas."
paul@1039 751
paul@1049 752
    # Quota and user identity/group discovery.
paul@1049 753
paul@1194 754
    get_quotas = Store.get_users
paul@1194 755
    get_quota_users = Store.get_freebusy_others
paul@1176 756
paul@1176 757
    # Delegate information for the quota.
paul@1176 758
paul@1176 759
    def get_delegates(self, quota):
paul@1176 760
paul@1176 761
        "Return a list of delegates for 'quota'."
paul@1176 762
paul@1176 763
        filename = self.get_object_in_store(quota, "delegates")
paul@1236 764
        if not filename:
paul@1176 765
            return []
paul@1176 766
paul@1236 767
        return FileTableSingle(filename)
paul@1176 768
paul@1176 769
    def set_delegates(self, quota, delegates):
paul@1176 770
paul@1176 771
        "For the given 'quota', set the list of 'delegates'."
paul@1176 772
paul@1176 773
        filename = self.get_object_in_store(quota, "delegates")
paul@1176 774
        if not filename:
paul@1176 775
            return False
paul@1176 776
paul@1236 777
        self.acquire_lock(quota)
paul@1236 778
        try:
paul@1236 779
            if not have_table(delegates, filename):
paul@1236 780
                de = self.get_delegates(quota)
paul@1236 781
                de.replaceall(delegates)
paul@1236 782
                delegates = de
paul@1236 783
            delegates.close()
paul@1236 784
        finally:
paul@1236 785
            self.release_lock(quota)
paul@1236 786
paul@1176 787
        return True
paul@1049 788
paul@1039 789
    # Groups of users sharing quotas.
paul@1039 790
paul@1039 791
    def get_groups(self, quota):
paul@1039 792
paul@1039 793
        "Return the identity mappings for the given 'quota' as a dictionary."
paul@1039 794
paul@1039 795
        filename = self.get_object_in_store(quota, "groups")
paul@1236 796
        if not filename:
paul@1039 797
            return {}
paul@1039 798
paul@1236 799
        return FileTableDict(filename, tab_separated=False)
paul@1039 800
paul@1176 801
    def set_groups(self, quota, groups):
paul@1142 802
paul@1176 803
        "For the given 'quota', set 'groups' mapping users to groups."
paul@1142 804
paul@1142 805
        filename = self.get_object_in_store(quota, "groups")
paul@1142 806
        if not filename:
paul@1142 807
            return False
paul@1142 808
paul@1236 809
        self.acquire_lock(quota)
paul@1236 810
        try:
paul@1236 811
            if not have_table(groups, filename):
paul@1236 812
                gr = self.get_groups(quota)
paul@1236 813
                gr.updateall(groups)
paul@1236 814
                groups = gr
paul@1236 815
            groups.close()
paul@1236 816
        finally:
paul@1236 817
            self.release_lock(quota)
paul@1236 818
paul@1142 819
        return True
paul@1142 820
paul@1039 821
    def get_limits(self, quota):
paul@1039 822
paul@1039 823
        """
paul@1039 824
        Return the limits for the 'quota' as a dictionary mapping identities or
paul@1039 825
        groups to durations.
paul@1039 826
        """
paul@1039 827
paul@1039 828
        filename = self.get_object_in_store(quota, "limits")
paul@1236 829
        if not filename:
paul@1142 830
            return {}
paul@1039 831
paul@1236 832
        return FileTableDict(filename, tab_separated=False)
paul@1039 833
paul@1176 834
    def set_limits(self, quota, limits):
paul@1089 835
paul@1089 836
        """
paul@1176 837
        For the given 'quota', set the given 'limits' on resource usage mapping
paul@1176 838
        groups to limits.
paul@1089 839
        """
paul@1089 840
paul@1089 841
        filename = self.get_object_in_store(quota, "limits")
paul@1089 842
        if not filename:
paul@1142 843
            return False
paul@1089 844
paul@1236 845
        self.acquire_lock(quota)
paul@1236 846
        try:
paul@1236 847
            if not have_table(limits, filename):
paul@1236 848
                li = self.get_limits(quota)
paul@1236 849
                li.updateall(limits)
paul@1236 850
                limits = li
paul@1236 851
            limits.close()
paul@1236 852
        finally:
paul@1236 853
            self.release_lock(quota)
paul@1236 854
paul@1089 855
        return True
paul@1089 856
paul@1039 857
    # Journal entry methods.
paul@1039 858
paul@1071 859
    def get_entries(self, quota, group, mutable=False):
paul@1039 860
paul@1039 861
        """
paul@1039 862
        Return a list of journal entries for the given 'quota' for the indicated
paul@1039 863
        'group'.
paul@1039 864
        """
paul@1039 865
paul@1193 866
        return self.get_freebusy_for_other(quota, group, mutable)
paul@1039 867
paul@1039 868
    def set_entries(self, quota, group, entries):
paul@1039 869
paul@1039 870
        """
paul@1039 871
        For the given 'quota' and indicated 'group', set the list of journal
paul@1039 872
        'entries'.
paul@1039 873
        """
paul@1039 874
paul@1192 875
        return self.set_freebusy_for_other(quota, entries, group)
paul@1039 876
paul@1193 877
    # Compatibility methods.
paul@1193 878
paul@1193 879
    def get_freebusy_for_other(self, user, other, mutable=False):
paul@1236 880
        return Store.get_freebusy_for_other(self, user, other, mutable, collection=FreeBusyGroupCollection)
paul@1236 881
paul@1236 882
    def set_freebusy_for_other(self, user, entries, other):
paul@1236 883
        Store.set_freebusy_for_other(self, user, entries, other, collection=FreeBusyGroupCollection)
paul@1193 884
paul@2 885
# vim: tabstop=4 expandtab shiftwidth=4