1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imiptools/freebusy/common.py Fri May 26 18:25:23 2017 +0200
1.3 @@ -0,0 +1,747 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Managing free/busy periods.
1.8 +
1.9 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
1.10 +
1.11 +This program is free software; you can redistribute it and/or modify it under
1.12 +the terms of the GNU General Public License as published by the Free Software
1.13 +Foundation; either version 3 of the License, or (at your option) any later
1.14 +version.
1.15 +
1.16 +This program is distributed in the hope that it will be useful, but WITHOUT
1.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1.19 +details.
1.20 +
1.21 +You should have received a copy of the GNU General Public License along with
1.22 +this program. If not, see <http://www.gnu.org/licenses/>.
1.23 +"""
1.24 +
1.25 +from bisect import bisect_left, bisect_right
1.26 +from imiptools.dates import format_datetime
1.27 +from imiptools.period import get_overlapping, Period, PeriodBase
1.28 +
1.29 +# Conversion functions.
1.30 +
1.31 +def from_string(s, encoding):
1.32 +
1.33 + "Interpret 's' using 'encoding', preserving None."
1.34 +
1.35 + if s:
1.36 + return unicode(s, encoding)
1.37 + else:
1.38 + return s
1.39 +
1.40 +def to_string(s, encoding):
1.41 +
1.42 + "Encode 's' using 'encoding', preserving None."
1.43 +
1.44 + if s:
1.45 + return s.encode(encoding)
1.46 + else:
1.47 + return s
1.48 +
1.49 +
1.50 +
1.51 +# Period abstractions.
1.52 +
1.53 +class FreeBusyPeriod(PeriodBase):
1.54 +
1.55 + "A free/busy record abstraction."
1.56 +
1.57 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
1.58 + summary=None, organiser=None):
1.59 +
1.60 + """
1.61 + Initialise a free/busy period with the given 'start' and 'end' points,
1.62 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
1.63 + details.
1.64 + """
1.65 +
1.66 + PeriodBase.__init__(self, start, end)
1.67 + self.uid = uid
1.68 + self.transp = transp or None
1.69 + self.recurrenceid = recurrenceid or None
1.70 + self.summary = summary or None
1.71 + self.organiser = organiser or None
1.72 +
1.73 + def as_tuple(self, strings_only=False, string_datetimes=False):
1.74 +
1.75 + """
1.76 + Return the initialisation parameter tuple, converting datetimes and
1.77 + false value parameters to strings if 'strings_only' is set to a true
1.78 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
1.79 + datetime values are converted to strings.
1.80 + """
1.81 +
1.82 + null = lambda x: (strings_only and [""] or [x])[0]
1.83 + return (
1.84 + (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
1.85 + (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
1.86 + self.uid or null(self.uid),
1.87 + self.transp or strings_only and "OPAQUE" or None,
1.88 + self.recurrenceid or null(self.recurrenceid),
1.89 + self.summary or null(self.summary),
1.90 + self.organiser or null(self.organiser)
1.91 + )
1.92 +
1.93 + def __cmp__(self, other):
1.94 +
1.95 + """
1.96 + Compare this object to 'other', employing the uid if the periods
1.97 + involved are the same.
1.98 + """
1.99 +
1.100 + result = PeriodBase.__cmp__(self, other)
1.101 + if result == 0 and isinstance(other, FreeBusyPeriod):
1.102 + return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))
1.103 + else:
1.104 + return result
1.105 +
1.106 + def get_key(self):
1.107 + return self.uid, self.recurrenceid, self.get_start()
1.108 +
1.109 + def __repr__(self):
1.110 + return "FreeBusyPeriod%r" % (self.as_tuple(),)
1.111 +
1.112 + def get_tzid(self):
1.113 + return "UTC"
1.114 +
1.115 + # Period and event recurrence logic.
1.116 +
1.117 + def is_replaced(self, recurrences):
1.118 +
1.119 + """
1.120 + Return whether this period refers to one of the 'recurrences'.
1.121 + The 'recurrences' must be UTC datetimes corresponding to the start of
1.122 + the period described by a recurrence.
1.123 + """
1.124 +
1.125 + for recurrence in recurrences:
1.126 + if self.is_affected(recurrence):
1.127 + return True
1.128 + return False
1.129 +
1.130 + def is_affected(self, recurrence):
1.131 +
1.132 + """
1.133 + Return whether this period refers to 'recurrence'. The 'recurrence' must
1.134 + be a UTC datetime corresponding to the start of the period described by
1.135 + a recurrence.
1.136 + """
1.137 +
1.138 + return recurrence and self.get_start_point() == recurrence
1.139 +
1.140 + # Value correction methods.
1.141 +
1.142 + def make_corrected(self, start, end):
1.143 + return self.__class__(start, end)
1.144 +
1.145 +class FreeBusyOfferPeriod(FreeBusyPeriod):
1.146 +
1.147 + "A free/busy record abstraction for an offer period."
1.148 +
1.149 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
1.150 + summary=None, organiser=None, expires=None):
1.151 +
1.152 + """
1.153 + Initialise a free/busy period with the given 'start' and 'end' points,
1.154 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
1.155 + details.
1.156 +
1.157 + An additional 'expires' parameter can be used to indicate an expiry
1.158 + datetime in conjunction with free/busy offers made when countering
1.159 + event proposals.
1.160 + """
1.161 +
1.162 + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
1.163 + summary, organiser)
1.164 + self.expires = expires or None
1.165 +
1.166 + def as_tuple(self, strings_only=False, string_datetimes=False):
1.167 +
1.168 + """
1.169 + Return the initialisation parameter tuple, converting datetimes and
1.170 + false value parameters to strings if 'strings_only' is set to a true
1.171 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
1.172 + datetime values are converted to strings.
1.173 + """
1.174 +
1.175 + null = lambda x: (strings_only and [""] or [x])[0]
1.176 + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
1.177 + self.expires or null(self.expires),)
1.178 +
1.179 + def __repr__(self):
1.180 + return "FreeBusyOfferPeriod%r" % (self.as_tuple(),)
1.181 +
1.182 +class FreeBusyGroupPeriod(FreeBusyPeriod):
1.183 +
1.184 + "A free/busy record abstraction for a quota group period."
1.185 +
1.186 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
1.187 + summary=None, organiser=None, attendee=None):
1.188 +
1.189 + """
1.190 + Initialise a free/busy period with the given 'start' and 'end' points,
1.191 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
1.192 + details.
1.193 +
1.194 + An additional 'attendee' parameter can be used to indicate the identity
1.195 + of the attendee recording the period.
1.196 + """
1.197 +
1.198 + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
1.199 + summary, organiser)
1.200 + self.attendee = attendee or None
1.201 +
1.202 + def as_tuple(self, strings_only=False, string_datetimes=False):
1.203 +
1.204 + """
1.205 + Return the initialisation parameter tuple, converting datetimes and
1.206 + false value parameters to strings if 'strings_only' is set to a true
1.207 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
1.208 + datetime values are converted to strings.
1.209 + """
1.210 +
1.211 + null = lambda x: (strings_only and [""] or [x])[0]
1.212 + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
1.213 + self.attendee or null(self.attendee),)
1.214 +
1.215 + def __cmp__(self, other):
1.216 +
1.217 + """
1.218 + Compare this object to 'other', employing the uid if the periods
1.219 + involved are the same.
1.220 + """
1.221 +
1.222 + result = FreeBusyPeriod.__cmp__(self, other)
1.223 + if isinstance(other, FreeBusyGroupPeriod) and result == 0:
1.224 + return cmp(self.attendee, other.attendee)
1.225 + else:
1.226 + return result
1.227 +
1.228 + def __repr__(self):
1.229 + return "FreeBusyGroupPeriod%r" % (self.as_tuple(),)
1.230 +
1.231 +class FreeBusyCollectionBase:
1.232 +
1.233 + "Common operations on free/busy period collections."
1.234 +
1.235 + period_columns = [
1.236 + "start", "end", "object_uid", "transp", "object_recurrenceid",
1.237 + "summary", "organiser"
1.238 + ]
1.239 +
1.240 + period_class = FreeBusyPeriod
1.241 +
1.242 + def __init__(self, mutable=True):
1.243 + self.mutable = mutable
1.244 +
1.245 + def _check_mutable(self):
1.246 + if not self.mutable:
1.247 + raise TypeError, "Cannot mutate this collection."
1.248 +
1.249 + def copy(self):
1.250 +
1.251 + "Make an independent mutable copy of the collection."
1.252 +
1.253 + return FreeBusyCollection(list(self), True)
1.254 +
1.255 + def make_period(self, t):
1.256 +
1.257 + """
1.258 + Make a period using the given tuple of arguments and the collection's
1.259 + column details.
1.260 + """
1.261 +
1.262 + args = []
1.263 + for arg, column in zip(t, self.period_columns):
1.264 + args.append(from_string(arg, "utf-8"))
1.265 + return self.period_class(*args)
1.266 +
1.267 + def make_tuple(self, t):
1.268 +
1.269 + """
1.270 + Return a tuple from the given tuple 't' conforming to the collection's
1.271 + column details.
1.272 + """
1.273 +
1.274 + args = []
1.275 + for arg, column in zip(t, self.period_columns):
1.276 + args.append(arg)
1.277 + return tuple(args)
1.278 +
1.279 + # List emulation methods.
1.280 +
1.281 + def __iadd__(self, periods):
1.282 + self.insert_periods(periods)
1.283 + return self
1.284 +
1.285 + def append(self, period):
1.286 + self.insert_period(period)
1.287 +
1.288 + # Operations.
1.289 +
1.290 + def insert_periods(self, periods):
1.291 +
1.292 + "Insert the given 'periods' into the collection."
1.293 +
1.294 + for p in periods:
1.295 + self.insert_period(p)
1.296 +
1.297 + def can_schedule(self, periods, uid, recurrenceid):
1.298 +
1.299 + """
1.300 + Return whether the collection can accommodate the given 'periods'
1.301 + employing the specified 'uid' and 'recurrenceid'.
1.302 + """
1.303 +
1.304 + for conflict in self.have_conflict(periods, True):
1.305 + if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
1.306 + return False
1.307 +
1.308 + return True
1.309 +
1.310 + def have_conflict(self, periods, get_conflicts=False):
1.311 +
1.312 + """
1.313 + Return whether any period in the collection overlaps with the given
1.314 + 'periods', returning a collection of such overlapping periods if
1.315 + 'get_conflicts' is set to a true value.
1.316 + """
1.317 +
1.318 + conflicts = set()
1.319 + for p in periods:
1.320 + overlapping = self.period_overlaps(p, get_conflicts)
1.321 + if overlapping:
1.322 + if get_conflicts:
1.323 + conflicts.update(overlapping)
1.324 + else:
1.325 + return True
1.326 +
1.327 + if get_conflicts:
1.328 + return conflicts
1.329 + else:
1.330 + return False
1.331 +
1.332 + def period_overlaps(self, period, get_periods=False):
1.333 +
1.334 + """
1.335 + Return whether any period in the collection overlaps with the given
1.336 + 'period', returning a collection of overlapping periods if 'get_periods'
1.337 + is set to a true value.
1.338 + """
1.339 +
1.340 + overlapping = self.get_overlapping([period])
1.341 +
1.342 + if get_periods:
1.343 + return overlapping
1.344 + else:
1.345 + return len(overlapping) != 0
1.346 +
1.347 + def replace_overlapping(self, period, replacements):
1.348 +
1.349 + """
1.350 + Replace existing periods in the collection within the given 'period',
1.351 + using the given 'replacements'.
1.352 + """
1.353 +
1.354 + self._check_mutable()
1.355 +
1.356 + self.remove_overlapping(period)
1.357 + for replacement in replacements:
1.358 + self.insert_period(replacement)
1.359 +
1.360 + def coalesce_freebusy(self):
1.361 +
1.362 + "Coalesce the periods in the collection, returning a new collection."
1.363 +
1.364 + if not self:
1.365 + return FreeBusyCollection()
1.366 +
1.367 + fb = []
1.368 +
1.369 + it = iter(self)
1.370 + period = it.next()
1.371 +
1.372 + start = period.get_start_point()
1.373 + end = period.get_end_point()
1.374 +
1.375 + try:
1.376 + while True:
1.377 + period = it.next()
1.378 + if period.get_start_point() > end:
1.379 + fb.append(self.period_class(start, end))
1.380 + start = period.get_start_point()
1.381 + end = period.get_end_point()
1.382 + else:
1.383 + end = max(end, period.get_end_point())
1.384 + except StopIteration:
1.385 + pass
1.386 +
1.387 + fb.append(self.period_class(start, end))
1.388 + return FreeBusyCollection(fb)
1.389 +
1.390 + def invert_freebusy(self):
1.391 +
1.392 + "Return the free periods from the collection as a new collection."
1.393 +
1.394 + if not self:
1.395 + return FreeBusyCollection([self.period_class(None, None)])
1.396 +
1.397 + # Coalesce periods that overlap or are adjacent.
1.398 +
1.399 + fb = self.coalesce_freebusy()
1.400 + free = []
1.401 +
1.402 + # Add a start-of-time period if appropriate.
1.403 +
1.404 + first = fb[0].get_start_point()
1.405 + if first:
1.406 + free.append(self.period_class(None, first))
1.407 +
1.408 + start = fb[0].get_end_point()
1.409 +
1.410 + for period in fb[1:]:
1.411 + free.append(self.period_class(start, period.get_start_point()))
1.412 + start = period.get_end_point()
1.413 +
1.414 + # Add an end-of-time period if appropriate.
1.415 +
1.416 + if start:
1.417 + free.append(self.period_class(start, None))
1.418 +
1.419 + return FreeBusyCollection(free)
1.420 +
1.421 + def _update_freebusy(self, periods, uid, recurrenceid):
1.422 +
1.423 + """
1.424 + Update the free/busy details with the given 'periods', using the given
1.425 + 'uid' plus 'recurrenceid' to remove existing periods.
1.426 + """
1.427 +
1.428 + self._check_mutable()
1.429 +
1.430 + self.remove_specific_event_periods(uid, recurrenceid)
1.431 +
1.432 + self.insert_periods(periods)
1.433 +
1.434 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser):
1.435 +
1.436 + """
1.437 + Update the free/busy details with the given 'periods', 'transp' setting,
1.438 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.439 + """
1.440 +
1.441 + new_periods = []
1.442 +
1.443 + for p in periods:
1.444 + new_periods.append(
1.445 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser)
1.446 + )
1.447 +
1.448 + self._update_freebusy(new_periods, uid, recurrenceid)
1.449 +
1.450 +class SupportAttendee:
1.451 +
1.452 + "A mix-in that supports the affected attendee in free/busy periods."
1.453 +
1.454 + period_columns = FreeBusyCollectionBase.period_columns + ["attendee"]
1.455 + period_class = FreeBusyGroupPeriod
1.456 +
1.457 + def _update_freebusy(self, periods, uid, recurrenceid, attendee=None):
1.458 +
1.459 + """
1.460 + Update the free/busy details with the given 'periods', using the given
1.461 + 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods.
1.462 + """
1.463 +
1.464 + self._check_mutable()
1.465 +
1.466 + self.remove_specific_event_periods(uid, recurrenceid, attendee)
1.467 +
1.468 + self.insert_periods(periods)
1.469 +
1.470 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None):
1.471 +
1.472 + """
1.473 + Update the free/busy details with the given 'periods', 'transp' setting,
1.474 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.475 +
1.476 + An optional 'attendee' indicates the attendee affected by the period.
1.477 + """
1.478 +
1.479 + new_periods = []
1.480 +
1.481 + for p in periods:
1.482 + new_periods.append(
1.483 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee)
1.484 + )
1.485 +
1.486 + self._update_freebusy(new_periods, uid, recurrenceid, attendee)
1.487 +
1.488 +class SupportExpires:
1.489 +
1.490 + "A mix-in that supports the expiry datetime in free/busy periods."
1.491 +
1.492 + period_columns = FreeBusyCollectionBase.period_columns + ["expires"]
1.493 + period_class = FreeBusyOfferPeriod
1.494 +
1.495 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
1.496 +
1.497 + """
1.498 + Update the free/busy details with the given 'periods', 'transp' setting,
1.499 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.500 +
1.501 + An optional 'expires' datetime string indicates the expiry time of any
1.502 + free/busy offer.
1.503 + """
1.504 +
1.505 + new_periods = []
1.506 +
1.507 + for p in periods:
1.508 + new_periods.append(
1.509 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)
1.510 + )
1.511 +
1.512 + self._update_freebusy(new_periods, uid, recurrenceid)
1.513 +
1.514 +
1.515 +
1.516 +# Simple abstractions suitable for use with file-based representations and as
1.517 +# general copies of collections.
1.518 +
1.519 +class FreeBusyCollection(FreeBusyCollectionBase):
1.520 +
1.521 + "An abstraction for a collection of free/busy periods."
1.522 +
1.523 + def __init__(self, periods=None, mutable=True):
1.524 +
1.525 + """
1.526 + Initialise the collection with the given list of 'periods', or start an
1.527 + empty collection if no list is given. If 'mutable' is indicated, the
1.528 + collection may be changed; otherwise, an exception will be raised.
1.529 + """
1.530 +
1.531 + FreeBusyCollectionBase.__init__(self, mutable)
1.532 + self.periods = periods or []
1.533 +
1.534 + # List emulation methods.
1.535 +
1.536 + def __nonzero__(self):
1.537 + return bool(self.periods)
1.538 +
1.539 + def __iter__(self):
1.540 + return iter(self.periods)
1.541 +
1.542 + def __len__(self):
1.543 + return len(self.periods)
1.544 +
1.545 + def __getitem__(self, i):
1.546 + return self.periods[i]
1.547 +
1.548 + # Operations.
1.549 +
1.550 + def insert_period(self, period):
1.551 +
1.552 + "Insert the given 'period' into the collection."
1.553 +
1.554 + self._check_mutable()
1.555 +
1.556 + i = bisect_left(self.periods, period)
1.557 + if i == len(self.periods):
1.558 + self.periods.append(period)
1.559 + elif self.periods[i] != period:
1.560 + self.periods.insert(i, period)
1.561 +
1.562 + def remove_periods(self, periods):
1.563 +
1.564 + "Remove the given 'periods' from the collection."
1.565 +
1.566 + self._check_mutable()
1.567 +
1.568 + for period in periods:
1.569 + i = bisect_left(self.periods, period)
1.570 + if i < len(self.periods) and self.periods[i] == period:
1.571 + del self.periods[i]
1.572 +
1.573 + def remove_event_periods(self, uid, recurrenceid=None, participant=None):
1.574 +
1.575 + """
1.576 + Remove from the collection all periods associated with 'uid' and
1.577 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.578 + be referenced).
1.579 +
1.580 + If 'participant' is specified, only remove periods for which the
1.581 + participant is given as attending.
1.582 +
1.583 + Return the removed periods.
1.584 + """
1.585 +
1.586 + self._check_mutable()
1.587 +
1.588 + removed = []
1.589 + i = 0
1.590 + while i < len(self.periods):
1.591 + fb = self.periods[i]
1.592 +
1.593 + if fb.uid == uid and fb.recurrenceid == recurrenceid and \
1.594 + (not participant or participant == fb.attendee):
1.595 +
1.596 + removed.append(self.periods[i])
1.597 + del self.periods[i]
1.598 + else:
1.599 + i += 1
1.600 +
1.601 + return removed
1.602 +
1.603 + # Specific period removal when updating event details.
1.604 +
1.605 + remove_specific_event_periods = remove_event_periods
1.606 +
1.607 + def remove_additional_periods(self, uid, recurrenceids=None):
1.608 +
1.609 + """
1.610 + Remove from the collection all periods associated with 'uid' having a
1.611 + recurrence identifier indicating an additional or modified period.
1.612 +
1.613 + If 'recurrenceids' is specified, remove all periods associated with
1.614 + 'uid' that do not have a recurrence identifier in the given list.
1.615 +
1.616 + Return the removed periods.
1.617 + """
1.618 +
1.619 + self._check_mutable()
1.620 +
1.621 + removed = []
1.622 + i = 0
1.623 + while i < len(self.periods):
1.624 + fb = self.periods[i]
1.625 + if fb.uid == uid and fb.recurrenceid and (
1.626 + recurrenceids is None or
1.627 + recurrenceids is not None and fb.recurrenceid not in recurrenceids
1.628 + ):
1.629 + removed.append(self.periods[i])
1.630 + del self.periods[i]
1.631 + else:
1.632 + i += 1
1.633 +
1.634 + return removed
1.635 +
1.636 + def remove_affected_period(self, uid, start, participant=None):
1.637 +
1.638 + """
1.639 + Remove from the collection the period associated with 'uid' that
1.640 + provides an occurrence starting at the given 'start' (provided by a
1.641 + recurrence identifier, converted to a datetime). A recurrence identifier
1.642 + is used to provide an alternative time period whilst also acting as a
1.643 + reference to the originally-defined occurrence.
1.644 +
1.645 + If 'participant' is specified, only remove periods for which the
1.646 + participant is given as attending.
1.647 +
1.648 + Return any removed period in a list.
1.649 + """
1.650 +
1.651 + self._check_mutable()
1.652 +
1.653 + removed = []
1.654 +
1.655 + search = Period(start, start)
1.656 + found = bisect_left(self.periods, search)
1.657 +
1.658 + while found < len(self.periods):
1.659 + fb = self.periods[found]
1.660 +
1.661 + # Stop looking if the start no longer matches the recurrence identifier.
1.662 +
1.663 + if fb.get_start_point() != search.get_start_point():
1.664 + break
1.665 +
1.666 + # If the period belongs to the parent object, remove it and return.
1.667 +
1.668 + if not fb.recurrenceid and uid == fb.uid and \
1.669 + (not participant or participant == fb.attendee):
1.670 +
1.671 + removed.append(self.periods[found])
1.672 + del self.periods[found]
1.673 + break
1.674 +
1.675 + # Otherwise, keep looking for a matching period.
1.676 +
1.677 + found += 1
1.678 +
1.679 + return removed
1.680 +
1.681 + def periods_from(self, period):
1.682 +
1.683 + "Return the entries in the collection at or after 'period'."
1.684 +
1.685 + first = bisect_left(self.periods, period)
1.686 + return self.periods[first:]
1.687 +
1.688 + def periods_until(self, period):
1.689 +
1.690 + "Return the entries in the collection before 'period'."
1.691 +
1.692 + last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
1.693 + return self.periods[:last]
1.694 +
1.695 + def get_overlapping(self, periods):
1.696 +
1.697 + """
1.698 + Return the entries in the collection providing periods overlapping with
1.699 + the given sorted collection of 'periods'.
1.700 + """
1.701 +
1.702 + return get_overlapping(self.periods, periods)
1.703 +
1.704 + def remove_overlapping(self, period):
1.705 +
1.706 + "Remove all periods overlapping with 'period' from the collection."
1.707 +
1.708 + self._check_mutable()
1.709 +
1.710 + overlapping = self.get_overlapping([period])
1.711 +
1.712 + if overlapping:
1.713 + for fb in overlapping:
1.714 + self.periods.remove(fb)
1.715 +
1.716 +class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection):
1.717 +
1.718 + "A collection of quota group free/busy objects."
1.719 +
1.720 + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
1.721 +
1.722 + """
1.723 + Remove from the collection all periods associated with 'uid' and
1.724 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.725 + be referenced) and any 'attendee'.
1.726 +
1.727 + Return the removed periods.
1.728 + """
1.729 +
1.730 + self._check_mutable()
1.731 +
1.732 + removed = []
1.733 + i = 0
1.734 + while i < len(self.periods):
1.735 + fb = self.periods[i]
1.736 + if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee:
1.737 + removed.append(self.periods[i])
1.738 + del self.periods[i]
1.739 + else:
1.740 + i += 1
1.741 +
1.742 + return removed
1.743 +
1.744 +class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection):
1.745 +
1.746 + "A collection of offered free/busy objects."
1.747 +
1.748 + pass
1.749 +
1.750 +# vim: tabstop=4 expandtab shiftwidth=4