imip-agent

Annotated imiptools/freebusy/common.py

1358:365abd0d41b4
2017-10-20 Paul Boddie Merged changes from the default branch. client-editing-simplification
paul@1234 1
#!/usr/bin/env python
paul@1234 2
paul@1234 3
"""
paul@1234 4
Managing free/busy periods.
paul@1234 5
paul@1234 6
Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
paul@1234 7
paul@1234 8
This program is free software; you can redistribute it and/or modify it under
paul@1234 9
the terms of the GNU General Public License as published by the Free Software
paul@1234 10
Foundation; either version 3 of the License, or (at your option) any later
paul@1234 11
version.
paul@1234 12
paul@1234 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@1234 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@1234 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@1234 16
details.
paul@1234 17
paul@1234 18
You should have received a copy of the GNU General Public License along with
paul@1234 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@1234 20
"""
paul@1234 21
paul@1234 22
from bisect import bisect_left, bisect_right
paul@1234 23
from imiptools.dates import format_datetime
paul@1234 24
from imiptools.period import get_overlapping, Period, PeriodBase
paul@1234 25
paul@1234 26
# Conversion functions.
paul@1234 27
paul@1234 28
def from_string(s, encoding):
paul@1234 29
paul@1234 30
    "Interpret 's' using 'encoding', preserving None."
paul@1234 31
paul@1234 32
    if s:
paul@1236 33
        if isinstance(s, unicode):
paul@1236 34
            return s
paul@1236 35
        else:
paul@1236 36
            return unicode(s, encoding)
paul@1234 37
    else:
paul@1234 38
        return s
paul@1234 39
paul@1234 40
def to_string(s, encoding):
paul@1234 41
paul@1234 42
    "Encode 's' using 'encoding', preserving None."
paul@1234 43
paul@1234 44
    if s:
paul@1234 45
        return s.encode(encoding)
paul@1234 46
    else:
paul@1234 47
        return s
paul@1234 48
paul@1236 49
class period_from_tuple:
paul@1236 50
paul@1236 51
    "Convert a tuple to an instance of the given 'period_class'."
paul@1236 52
paul@1236 53
    def __init__(self, period_class):
paul@1236 54
        self.period_class = period_class
paul@1236 55
    def __call__(self, t):
paul@1236 56
        return make_period(t, self.period_class)
paul@1236 57
paul@1236 58
def period_to_tuple(p):
paul@1236 59
paul@1236 60
    "Convert period 'p' to a tuple for serialisation."
paul@1236 61
paul@1236 62
    return p.as_tuple(strings_only=True)
paul@1236 63
paul@1236 64
def make_period(t, period_class):
paul@1236 65
paul@1236 66
    "Convert tuple 't' to an instance of the given 'period_class'."
paul@1236 67
paul@1236 68
    args = []
paul@1236 69
    for arg, column in zip(t, period_class.period_columns):
paul@1236 70
        args.append(from_string(arg, "utf-8"))
paul@1236 71
    return period_class(*args)
paul@1236 72
paul@1236 73
def make_tuple(t, period_class):
paul@1236 74
paul@1236 75
    "Restrict tuple 't' to the columns appropriate for 'period_class'."
paul@1236 76
paul@1236 77
    args = []
paul@1236 78
    for arg, column in zip(t, period_class.period_columns):
paul@1236 79
        args.append(arg)
paul@1236 80
    return tuple(args)
paul@1236 81
paul@1234 82
paul@1234 83
paul@1234 84
# Period abstractions.
paul@1234 85
paul@1234 86
class FreeBusyPeriod(PeriodBase):
paul@1234 87
paul@1234 88
    "A free/busy record abstraction."
paul@1234 89
paul@1236 90
    period_columns = [
paul@1236 91
        "start", "end", "object_uid", "transp", "object_recurrenceid",
paul@1236 92
        "summary", "organiser"
paul@1236 93
        ]
paul@1236 94
paul@1234 95
    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
paul@1234 96
        summary=None, organiser=None):
paul@1234 97
paul@1234 98
        """
paul@1234 99
        Initialise a free/busy period with the given 'start' and 'end' points,
paul@1234 100
        plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
paul@1234 101
        details.
paul@1234 102
        """
paul@1234 103
paul@1234 104
        PeriodBase.__init__(self, start, end)
paul@1236 105
        self.uid = uid or None
paul@1234 106
        self.transp = transp or None
paul@1234 107
        self.recurrenceid = recurrenceid or None
paul@1234 108
        self.summary = summary or None
paul@1234 109
        self.organiser = organiser or None
paul@1234 110
paul@1234 111
    def as_tuple(self, strings_only=False, string_datetimes=False):
paul@1234 112
paul@1234 113
        """
paul@1234 114
        Return the initialisation parameter tuple, converting datetimes and
paul@1234 115
        false value parameters to strings if 'strings_only' is set to a true
paul@1234 116
        value. Otherwise, if 'string_datetimes' is set to a true value, only the
paul@1234 117
        datetime values are converted to strings.
paul@1234 118
        """
paul@1234 119
paul@1234 120
        null = lambda x: (strings_only and [""] or [x])[0]
paul@1234 121
        return (
paul@1234 122
            (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
paul@1234 123
            (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
paul@1234 124
            self.uid or null(self.uid),
paul@1234 125
            self.transp or strings_only and "OPAQUE" or None,
paul@1234 126
            self.recurrenceid or null(self.recurrenceid),
paul@1234 127
            self.summary or null(self.summary),
paul@1234 128
            self.organiser or null(self.organiser)
paul@1234 129
            )
paul@1234 130
paul@1234 131
    def __cmp__(self, other):
paul@1234 132
paul@1234 133
        """
paul@1234 134
        Compare this object to 'other', employing the uid if the periods
paul@1234 135
        involved are the same.
paul@1234 136
        """
paul@1234 137
paul@1234 138
        result = PeriodBase.__cmp__(self, other)
paul@1234 139
        if result == 0 and isinstance(other, FreeBusyPeriod):
paul@1234 140
            return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))
paul@1234 141
        else:
paul@1234 142
            return result
paul@1234 143
paul@1234 144
    def get_key(self):
paul@1234 145
        return self.uid, self.recurrenceid, self.get_start()
paul@1234 146
paul@1234 147
    def __repr__(self):
paul@1234 148
        return "FreeBusyPeriod%r" % (self.as_tuple(),)
paul@1234 149
paul@1234 150
    def get_tzid(self):
paul@1234 151
        return "UTC"
paul@1234 152
paul@1234 153
    # Period and event recurrence logic.
paul@1234 154
paul@1234 155
    def is_replaced(self, recurrences):
paul@1234 156
paul@1234 157
        """
paul@1234 158
        Return whether this period refers to one of the 'recurrences'.
paul@1234 159
        The 'recurrences' must be UTC datetimes corresponding to the start of
paul@1234 160
        the period described by a recurrence.
paul@1234 161
        """
paul@1234 162
paul@1234 163
        for recurrence in recurrences:
paul@1234 164
            if self.is_affected(recurrence):
paul@1234 165
                return True
paul@1234 166
        return False
paul@1234 167
paul@1234 168
    def is_affected(self, recurrence):
paul@1234 169
paul@1234 170
        """
paul@1234 171
        Return whether this period refers to 'recurrence'. The 'recurrence' must
paul@1234 172
        be a UTC datetime corresponding to the start of the period described by
paul@1234 173
        a recurrence.
paul@1234 174
        """
paul@1234 175
paul@1234 176
        return recurrence and self.get_start_point() == recurrence
paul@1234 177
paul@1234 178
    # Value correction methods.
paul@1234 179
paul@1234 180
    def make_corrected(self, start, end):
paul@1234 181
        return self.__class__(start, end)
paul@1234 182
paul@1234 183
class FreeBusyOfferPeriod(FreeBusyPeriod):
paul@1234 184
paul@1234 185
    "A free/busy record abstraction for an offer period."
paul@1234 186
paul@1236 187
    period_columns = FreeBusyPeriod.period_columns + ["expires"]
paul@1236 188
paul@1234 189
    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
paul@1234 190
        summary=None, organiser=None, expires=None):
paul@1234 191
paul@1234 192
        """
paul@1234 193
        Initialise a free/busy period with the given 'start' and 'end' points,
paul@1234 194
        plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
paul@1234 195
        details.
paul@1234 196
paul@1234 197
        An additional 'expires' parameter can be used to indicate an expiry
paul@1234 198
        datetime in conjunction with free/busy offers made when countering
paul@1234 199
        event proposals.
paul@1234 200
        """
paul@1234 201
paul@1234 202
        FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
paul@1234 203
            summary, organiser)
paul@1234 204
        self.expires = expires or None
paul@1234 205
paul@1234 206
    def as_tuple(self, strings_only=False, string_datetimes=False):
paul@1234 207
paul@1234 208
        """
paul@1234 209
        Return the initialisation parameter tuple, converting datetimes and
paul@1234 210
        false value parameters to strings if 'strings_only' is set to a true
paul@1234 211
        value. Otherwise, if 'string_datetimes' is set to a true value, only the
paul@1234 212
        datetime values are converted to strings.
paul@1234 213
        """
paul@1234 214
paul@1234 215
        null = lambda x: (strings_only and [""] or [x])[0]
paul@1234 216
        return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
paul@1234 217
            self.expires or null(self.expires),)
paul@1234 218
paul@1234 219
    def __repr__(self):
paul@1234 220
        return "FreeBusyOfferPeriod%r" % (self.as_tuple(),)
paul@1234 221
paul@1234 222
class FreeBusyGroupPeriod(FreeBusyPeriod):
paul@1234 223
paul@1234 224
    "A free/busy record abstraction for a quota group period."
paul@1234 225
paul@1236 226
    period_columns = FreeBusyPeriod.period_columns + ["attendee"]
paul@1236 227
paul@1234 228
    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
paul@1234 229
        summary=None, organiser=None, attendee=None):
paul@1234 230
paul@1234 231
        """
paul@1234 232
        Initialise a free/busy period with the given 'start' and 'end' points,
paul@1234 233
        plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
paul@1234 234
        details.
paul@1234 235
paul@1234 236
        An additional 'attendee' parameter can be used to indicate the identity
paul@1234 237
        of the attendee recording the period.
paul@1234 238
        """
paul@1234 239
paul@1234 240
        FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
paul@1234 241
            summary, organiser)
paul@1234 242
        self.attendee = attendee or None
paul@1234 243
paul@1234 244
    def as_tuple(self, strings_only=False, string_datetimes=False):
paul@1234 245
paul@1234 246
        """
paul@1234 247
        Return the initialisation parameter tuple, converting datetimes and
paul@1234 248
        false value parameters to strings if 'strings_only' is set to a true
paul@1234 249
        value. Otherwise, if 'string_datetimes' is set to a true value, only the
paul@1234 250
        datetime values are converted to strings.
paul@1234 251
        """
paul@1234 252
paul@1234 253
        null = lambda x: (strings_only and [""] or [x])[0]
paul@1234 254
        return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
paul@1234 255
            self.attendee or null(self.attendee),)
paul@1234 256
paul@1234 257
    def __cmp__(self, other):
paul@1234 258
paul@1234 259
        """
paul@1234 260
        Compare this object to 'other', employing the uid if the periods
paul@1234 261
        involved are the same.
paul@1234 262
        """
paul@1234 263
paul@1234 264
        result = FreeBusyPeriod.__cmp__(self, other)
paul@1234 265
        if isinstance(other, FreeBusyGroupPeriod) and result == 0:
paul@1234 266
            return cmp(self.attendee, other.attendee)
paul@1234 267
        else:
paul@1234 268
            return result
paul@1234 269
paul@1234 270
    def __repr__(self):
paul@1234 271
        return "FreeBusyGroupPeriod%r" % (self.as_tuple(),)
paul@1234 272
paul@1236 273
paul@1236 274
paul@1236 275
# Collection abstractions.
paul@1236 276
paul@1234 277
class FreeBusyCollectionBase:
paul@1234 278
paul@1234 279
    "Common operations on free/busy period collections."
paul@1234 280
paul@1234 281
    period_class = FreeBusyPeriod
paul@1234 282
paul@1234 283
    def __init__(self, mutable=True):
paul@1234 284
        self.mutable = mutable
paul@1234 285
paul@1234 286
    def _check_mutable(self):
paul@1234 287
        if not self.mutable:
paul@1234 288
            raise TypeError, "Cannot mutate this collection."
paul@1234 289
paul@1236 290
    def close(self):
paul@1236 291
paul@1236 292
        "Close the collection."
paul@1236 293
paul@1236 294
        pass
paul@1236 295
paul@1234 296
    def copy(self):
paul@1234 297
paul@1234 298
        "Make an independent mutable copy of the collection."
paul@1234 299
paul@1234 300
        return FreeBusyCollection(list(self), True)
paul@1234 301
paul@1234 302
    def make_period(self, t):
paul@1234 303
paul@1234 304
        """
paul@1234 305
        Make a period using the given tuple of arguments and the collection's
paul@1234 306
        column details.
paul@1234 307
        """
paul@1234 308
paul@1236 309
        return make_period(t, self.period_class)
paul@1234 310
paul@1234 311
    def make_tuple(self, t):
paul@1234 312
paul@1234 313
        """
paul@1234 314
        Return a tuple from the given tuple 't' conforming to the collection's
paul@1234 315
        column details.
paul@1234 316
        """
paul@1234 317
paul@1236 318
        return make_tuple(t, self.period_class)
paul@1234 319
paul@1234 320
    # List emulation methods.
paul@1234 321
paul@1234 322
    def __iadd__(self, periods):
paul@1234 323
        self.insert_periods(periods)
paul@1234 324
        return self
paul@1234 325
paul@1234 326
    def append(self, period):
paul@1234 327
        self.insert_period(period)
paul@1234 328
paul@1234 329
    # Operations.
paul@1234 330
paul@1236 331
    def insert_period(self, period):
paul@1236 332
paul@1236 333
        """
paul@1236 334
        Insert the given 'period' into the collection.
paul@1236 335
paul@1236 336
        This should be implemented in subclasses.
paul@1236 337
        """
paul@1236 338
paul@1236 339
        pass
paul@1236 340
paul@1234 341
    def insert_periods(self, periods):
paul@1234 342
paul@1234 343
        "Insert the given 'periods' into the collection."
paul@1234 344
paul@1234 345
        for p in periods:
paul@1234 346
            self.insert_period(p)
paul@1234 347
paul@1234 348
    def can_schedule(self, periods, uid, recurrenceid):
paul@1234 349
paul@1234 350
        """
paul@1234 351
        Return whether the collection can accommodate the given 'periods'
paul@1234 352
        employing the specified 'uid' and 'recurrenceid'.
paul@1234 353
        """
paul@1234 354
paul@1234 355
        for conflict in self.have_conflict(periods, True):
paul@1234 356
            if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
paul@1234 357
                return False
paul@1234 358
paul@1234 359
        return True
paul@1234 360
paul@1234 361
    def have_conflict(self, periods, get_conflicts=False):
paul@1234 362
paul@1234 363
        """
paul@1234 364
        Return whether any period in the collection overlaps with the given
paul@1234 365
        'periods', returning a collection of such overlapping periods if
paul@1234 366
        'get_conflicts' is set to a true value.
paul@1234 367
        """
paul@1234 368
paul@1234 369
        conflicts = set()
paul@1234 370
        for p in periods:
paul@1234 371
            overlapping = self.period_overlaps(p, get_conflicts)
paul@1234 372
            if overlapping:
paul@1234 373
                if get_conflicts:
paul@1234 374
                    conflicts.update(overlapping)
paul@1234 375
                else:
paul@1234 376
                    return True
paul@1234 377
paul@1234 378
        if get_conflicts:
paul@1234 379
            return conflicts
paul@1234 380
        else:
paul@1234 381
            return False
paul@1234 382
paul@1234 383
    def period_overlaps(self, period, get_periods=False):
paul@1234 384
paul@1234 385
        """
paul@1234 386
        Return whether any period in the collection overlaps with the given
paul@1234 387
        'period', returning a collection of overlapping periods if 'get_periods'
paul@1234 388
        is set to a true value.
paul@1234 389
        """
paul@1234 390
paul@1234 391
        overlapping = self.get_overlapping([period])
paul@1234 392
paul@1234 393
        if get_periods:
paul@1234 394
            return overlapping
paul@1234 395
        else:
paul@1234 396
            return len(overlapping) != 0
paul@1234 397
paul@1234 398
    def replace_overlapping(self, period, replacements):
paul@1234 399
paul@1234 400
        """
paul@1234 401
        Replace existing periods in the collection within the given 'period',
paul@1234 402
        using the given 'replacements'.
paul@1234 403
        """
paul@1234 404
paul@1234 405
        self._check_mutable()
paul@1234 406
paul@1234 407
        self.remove_overlapping(period)
paul@1234 408
        for replacement in replacements:
paul@1234 409
            self.insert_period(replacement)
paul@1234 410
paul@1234 411
    def coalesce_freebusy(self):
paul@1234 412
paul@1234 413
        "Coalesce the periods in the collection, returning a new collection."
paul@1234 414
paul@1234 415
        if not self:
paul@1234 416
            return FreeBusyCollection()
paul@1234 417
paul@1234 418
        fb = []
paul@1234 419
paul@1234 420
        it = iter(self)
paul@1234 421
        period = it.next()
paul@1234 422
paul@1234 423
        start = period.get_start_point()
paul@1234 424
        end = period.get_end_point()
paul@1234 425
paul@1234 426
        try:
paul@1234 427
            while True:
paul@1234 428
                period = it.next()
paul@1234 429
                if period.get_start_point() > end:
paul@1234 430
                    fb.append(self.period_class(start, end))
paul@1234 431
                    start = period.get_start_point()
paul@1234 432
                    end = period.get_end_point()
paul@1234 433
                else:
paul@1234 434
                    end = max(end, period.get_end_point())
paul@1234 435
        except StopIteration:
paul@1234 436
            pass
paul@1234 437
paul@1234 438
        fb.append(self.period_class(start, end))
paul@1234 439
        return FreeBusyCollection(fb)
paul@1234 440
paul@1234 441
    def invert_freebusy(self):
paul@1234 442
paul@1234 443
        "Return the free periods from the collection as a new collection."
paul@1234 444
paul@1234 445
        if not self:
paul@1234 446
            return FreeBusyCollection([self.period_class(None, None)])
paul@1234 447
paul@1234 448
        # Coalesce periods that overlap or are adjacent.
paul@1234 449
paul@1234 450
        fb = self.coalesce_freebusy()
paul@1234 451
        free = []
paul@1234 452
paul@1234 453
        # Add a start-of-time period if appropriate.
paul@1234 454
paul@1234 455
        first = fb[0].get_start_point()
paul@1234 456
        if first:
paul@1234 457
            free.append(self.period_class(None, first))
paul@1234 458
paul@1234 459
        start = fb[0].get_end_point()
paul@1234 460
paul@1234 461
        for period in fb[1:]:
paul@1234 462
            free.append(self.period_class(start, period.get_start_point()))
paul@1234 463
            start = period.get_end_point()
paul@1234 464
paul@1234 465
        # Add an end-of-time period if appropriate.
paul@1234 466
paul@1234 467
        if start:
paul@1234 468
            free.append(self.period_class(start, None))
paul@1234 469
paul@1234 470
        return FreeBusyCollection(free)
paul@1234 471
paul@1234 472
    def _update_freebusy(self, periods, uid, recurrenceid):
paul@1234 473
paul@1234 474
        """
paul@1234 475
        Update the free/busy details with the given 'periods', using the given
paul@1234 476
        'uid' plus 'recurrenceid' to remove existing periods.
paul@1234 477
        """
paul@1234 478
paul@1234 479
        self._check_mutable()
paul@1234 480
paul@1234 481
        self.remove_specific_event_periods(uid, recurrenceid)
paul@1234 482
paul@1234 483
        self.insert_periods(periods)
paul@1234 484
paul@1234 485
    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser):
paul@1234 486
paul@1234 487
        """
paul@1234 488
        Update the free/busy details with the given 'periods', 'transp' setting,
paul@1234 489
        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
paul@1234 490
        """
paul@1234 491
paul@1234 492
        new_periods = []
paul@1234 493
paul@1234 494
        for p in periods:
paul@1234 495
            new_periods.append(
paul@1234 496
                self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser)
paul@1234 497
                )
paul@1234 498
paul@1234 499
        self._update_freebusy(new_periods, uid, recurrenceid)
paul@1234 500
paul@1234 501
class SupportAttendee:
paul@1234 502
paul@1234 503
    "A mix-in that supports the affected attendee in free/busy periods."
paul@1234 504
paul@1234 505
    period_class = FreeBusyGroupPeriod
paul@1234 506
paul@1234 507
    def _update_freebusy(self, periods, uid, recurrenceid, attendee=None):
paul@1234 508
paul@1234 509
        """
paul@1234 510
        Update the free/busy details with the given 'periods', using the given
paul@1234 511
        'uid' plus 'recurrenceid' and 'attendee' to remove existing periods.
paul@1234 512
        """
paul@1234 513
paul@1234 514
        self._check_mutable()
paul@1234 515
paul@1234 516
        self.remove_specific_event_periods(uid, recurrenceid, attendee)
paul@1234 517
paul@1234 518
        self.insert_periods(periods)
paul@1234 519
paul@1234 520
    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None):
paul@1234 521
paul@1234 522
        """
paul@1234 523
        Update the free/busy details with the given 'periods', 'transp' setting,
paul@1234 524
        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
paul@1234 525
paul@1234 526
        An optional 'attendee' indicates the attendee affected by the period.
paul@1234 527
        """
paul@1234 528
paul@1234 529
        new_periods = []
paul@1234 530
paul@1234 531
        for p in periods:
paul@1234 532
            new_periods.append(
paul@1234 533
                self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee)
paul@1234 534
                )
paul@1234 535
paul@1234 536
        self._update_freebusy(new_periods, uid, recurrenceid, attendee)
paul@1234 537
paul@1234 538
class SupportExpires:
paul@1234 539
paul@1234 540
    "A mix-in that supports the expiry datetime in free/busy periods."
paul@1234 541
paul@1234 542
    period_class = FreeBusyOfferPeriod
paul@1234 543
paul@1234 544
    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
paul@1234 545
paul@1234 546
        """
paul@1234 547
        Update the free/busy details with the given 'periods', 'transp' setting,
paul@1234 548
        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
paul@1234 549
paul@1234 550
        An optional 'expires' datetime string indicates the expiry time of any
paul@1234 551
        free/busy offer.
paul@1234 552
        """
paul@1234 553
paul@1234 554
        new_periods = []
paul@1234 555
paul@1234 556
        for p in periods:
paul@1234 557
            new_periods.append(
paul@1234 558
                self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)
paul@1234 559
                )
paul@1234 560
paul@1234 561
        self._update_freebusy(new_periods, uid, recurrenceid)
paul@1234 562
paul@1234 563
paul@1234 564
paul@1234 565
# Simple abstractions suitable for use with file-based representations and as
paul@1234 566
# general copies of collections.
paul@1234 567
paul@1234 568
class FreeBusyCollection(FreeBusyCollectionBase):
paul@1234 569
paul@1234 570
    "An abstraction for a collection of free/busy periods."
paul@1234 571
paul@1234 572
    def __init__(self, periods=None, mutable=True):
paul@1234 573
paul@1234 574
        """
paul@1234 575
        Initialise the collection with the given list of 'periods', or start an
paul@1234 576
        empty collection if no list is given. If 'mutable' is indicated, the
paul@1234 577
        collection may be changed; otherwise, an exception will be raised.
paul@1234 578
        """
paul@1234 579
paul@1234 580
        FreeBusyCollectionBase.__init__(self, mutable)
paul@1236 581
paul@1236 582
        if periods is not None:
paul@1236 583
            self.periods = periods
paul@1236 584
        else:
paul@1236 585
            self.periods = []
paul@1236 586
paul@1236 587
    def get_filename(self):
paul@1236 588
paul@1236 589
        "Return any filename for the periods collection."
paul@1236 590
paul@1236 591
        if hasattr(self.periods, "filename"):
paul@1236 592
            return self.periods.filename
paul@1236 593
        else:
paul@1236 594
            return None
paul@1236 595
paul@1236 596
    def close(self):
paul@1236 597
paul@1236 598
        "Close the collection."
paul@1236 599
paul@1236 600
        if hasattr(self.periods, "close"):
paul@1236 601
            self.periods.close()
paul@1234 602
paul@1234 603
    # List emulation methods.
paul@1234 604
paul@1234 605
    def __nonzero__(self):
paul@1234 606
        return bool(self.periods)
paul@1234 607
paul@1234 608
    def __iter__(self):
paul@1234 609
        return iter(self.periods)
paul@1234 610
paul@1234 611
    def __len__(self):
paul@1234 612
        return len(self.periods)
paul@1234 613
paul@1234 614
    def __getitem__(self, i):
paul@1234 615
        return self.periods[i]
paul@1234 616
paul@1236 617
    # Dictionary emulation methods (even though this is not a mapping).
paul@1236 618
paul@1236 619
    def clear(self):
paul@1236 620
        del self.periods[:]
paul@1236 621
paul@1234 622
    # Operations.
paul@1234 623
paul@1234 624
    def insert_period(self, period):
paul@1234 625
paul@1234 626
        "Insert the given 'period' into the collection."
paul@1234 627
paul@1234 628
        self._check_mutable()
paul@1234 629
paul@1234 630
        i = bisect_left(self.periods, period)
paul@1234 631
        if i == len(self.periods):
paul@1234 632
            self.periods.append(period)
paul@1234 633
        elif self.periods[i] != period:
paul@1234 634
            self.periods.insert(i, period)
paul@1234 635
paul@1234 636
    def remove_periods(self, periods):
paul@1234 637
paul@1234 638
        "Remove the given 'periods' from the collection."
paul@1234 639
paul@1234 640
        self._check_mutable()
paul@1234 641
paul@1234 642
        for period in periods:
paul@1234 643
            i = bisect_left(self.periods, period)
paul@1234 644
            if i < len(self.periods) and self.periods[i] == period:
paul@1234 645
                del self.periods[i]
paul@1234 646
paul@1243 647
    def remove_periods_before(self, period):
paul@1243 648
paul@1243 649
        "Remove the entries in the collection before 'period'."
paul@1243 650
paul@1243 651
        last = bisect_right(self.periods, period)
paul@1243 652
        self.remove_periods(self.periods[:last])
paul@1243 653
paul@1234 654
    def remove_event_periods(self, uid, recurrenceid=None, participant=None):
paul@1234 655
paul@1234 656
        """
paul@1234 657
        Remove from the collection all periods associated with 'uid' and
paul@1234 658
        'recurrenceid' (which if omitted causes the "parent" object's periods to
paul@1234 659
        be referenced).
paul@1234 660
paul@1234 661
        If 'participant' is specified, only remove periods for which the
paul@1234 662
        participant is given as attending.
paul@1234 663
paul@1234 664
        Return the removed periods.
paul@1234 665
        """
paul@1234 666
paul@1234 667
        self._check_mutable()
paul@1234 668
paul@1234 669
        removed = []
paul@1234 670
        i = 0
paul@1234 671
        while i < len(self.periods):
paul@1234 672
            fb = self.periods[i]
paul@1234 673
paul@1234 674
            if fb.uid == uid and fb.recurrenceid == recurrenceid and \
paul@1234 675
               (not participant or participant == fb.attendee):
paul@1234 676
paul@1234 677
                removed.append(self.periods[i])
paul@1234 678
                del self.periods[i]
paul@1234 679
            else:
paul@1234 680
                i += 1
paul@1234 681
paul@1234 682
        return removed
paul@1234 683
paul@1234 684
    # Specific period removal when updating event details.
paul@1234 685
paul@1234 686
    remove_specific_event_periods = remove_event_periods
paul@1234 687
paul@1234 688
    def remove_additional_periods(self, uid, recurrenceids=None):
paul@1234 689
paul@1234 690
        """
paul@1234 691
        Remove from the collection all periods associated with 'uid' having a
paul@1234 692
        recurrence identifier indicating an additional or modified period.
paul@1234 693
paul@1234 694
        If 'recurrenceids' is specified, remove all periods associated with
paul@1234 695
        'uid' that do not have a recurrence identifier in the given list.
paul@1234 696
paul@1234 697
        Return the removed periods.
paul@1234 698
        """
paul@1234 699
paul@1234 700
        self._check_mutable()
paul@1234 701
paul@1234 702
        removed = []
paul@1234 703
        i = 0
paul@1234 704
        while i < len(self.periods):
paul@1234 705
            fb = self.periods[i]
paul@1234 706
            if fb.uid == uid and fb.recurrenceid and (
paul@1234 707
                recurrenceids is None or
paul@1234 708
                recurrenceids is not None and fb.recurrenceid not in recurrenceids
paul@1234 709
                ):
paul@1234 710
                removed.append(self.periods[i])
paul@1234 711
                del self.periods[i]
paul@1234 712
            else:
paul@1234 713
                i += 1
paul@1234 714
paul@1234 715
        return removed
paul@1234 716
paul@1234 717
    def remove_affected_period(self, uid, start, participant=None):
paul@1234 718
paul@1234 719
        """
paul@1234 720
        Remove from the collection the period associated with 'uid' that
paul@1234 721
        provides an occurrence starting at the given 'start' (provided by a
paul@1234 722
        recurrence identifier, converted to a datetime). A recurrence identifier
paul@1234 723
        is used to provide an alternative time period whilst also acting as a
paul@1234 724
        reference to the originally-defined occurrence.
paul@1234 725
paul@1234 726
        If 'participant' is specified, only remove periods for which the
paul@1234 727
        participant is given as attending.
paul@1234 728
paul@1234 729
        Return any removed period in a list.
paul@1234 730
        """
paul@1234 731
paul@1234 732
        self._check_mutable()
paul@1234 733
paul@1234 734
        removed = []
paul@1234 735
paul@1234 736
        search = Period(start, start)
paul@1234 737
        found = bisect_left(self.periods, search)
paul@1234 738
paul@1234 739
        while found < len(self.periods):
paul@1234 740
            fb = self.periods[found]
paul@1234 741
paul@1234 742
            # Stop looking if the start no longer matches the recurrence identifier.
paul@1234 743
paul@1234 744
            if fb.get_start_point() != search.get_start_point():
paul@1234 745
                break
paul@1234 746
paul@1234 747
            # If the period belongs to the parent object, remove it and return.
paul@1234 748
paul@1234 749
            if not fb.recurrenceid and uid == fb.uid and \
paul@1234 750
               (not participant or participant == fb.attendee):
paul@1234 751
paul@1234 752
                removed.append(self.periods[found])
paul@1234 753
                del self.periods[found]
paul@1234 754
                break
paul@1234 755
paul@1234 756
            # Otherwise, keep looking for a matching period.
paul@1234 757
paul@1234 758
            found += 1
paul@1234 759
paul@1234 760
        return removed
paul@1234 761
paul@1234 762
    def periods_from(self, period):
paul@1234 763
paul@1234 764
        "Return the entries in the collection at or after 'period'."
paul@1234 765
paul@1234 766
        first = bisect_left(self.periods, period)
paul@1234 767
        return self.periods[first:]
paul@1234 768
paul@1234 769
    def periods_until(self, period):
paul@1234 770
paul@1234 771
        "Return the entries in the collection before 'period'."
paul@1234 772
paul@1234 773
        last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
paul@1234 774
        return self.periods[:last]
paul@1234 775
paul@1234 776
    def get_overlapping(self, periods):
paul@1234 777
paul@1234 778
        """
paul@1234 779
        Return the entries in the collection providing periods overlapping with
paul@1234 780
        the given sorted collection of 'periods'.
paul@1234 781
        """
paul@1234 782
paul@1234 783
        return get_overlapping(self.periods, periods)
paul@1234 784
paul@1234 785
    def remove_overlapping(self, period):
paul@1234 786
paul@1234 787
        "Remove all periods overlapping with 'period' from the collection."
paul@1234 788
paul@1234 789
        self._check_mutable()
paul@1234 790
paul@1234 791
        overlapping = self.get_overlapping([period])
paul@1234 792
paul@1234 793
        if overlapping:
paul@1234 794
            for fb in overlapping:
paul@1234 795
                self.periods.remove(fb)
paul@1234 796
paul@1234 797
class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection):
paul@1234 798
paul@1234 799
    "A collection of quota group free/busy objects."
paul@1234 800
paul@1234 801
    def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
paul@1234 802
paul@1234 803
        """
paul@1234 804
        Remove from the collection all periods associated with 'uid' and
paul@1234 805
        'recurrenceid' (which if omitted causes the "parent" object's periods to
paul@1234 806
        be referenced) and any 'attendee'.
paul@1234 807
paul@1234 808
        Return the removed periods.
paul@1234 809
        """
paul@1234 810
paul@1234 811
        self._check_mutable()
paul@1234 812
paul@1234 813
        removed = []
paul@1234 814
        i = 0
paul@1234 815
        while i < len(self.periods):
paul@1234 816
            fb = self.periods[i]
paul@1234 817
            if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee:
paul@1234 818
                removed.append(self.periods[i])
paul@1234 819
                del self.periods[i]
paul@1234 820
            else:
paul@1234 821
                i += 1
paul@1234 822
paul@1234 823
        return removed
paul@1234 824
paul@1234 825
class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection):
paul@1234 826
paul@1234 827
    "A collection of offered free/busy objects."
paul@1234 828
paul@1234 829
    pass
paul@1234 830
paul@1234 831
# vim: tabstop=4 expandtab shiftwidth=4