1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imiptools/freebusy.py Tue May 23 16:34:09 2017 +0200
1.3 @@ -0,0 +1,1079 @@
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.sql import DatabaseOperations
1.28 +
1.29 +def from_string(s, encoding):
1.30 + if s:
1.31 + return unicode(s, encoding)
1.32 + else:
1.33 + return s
1.34 +
1.35 +def to_string(s, encoding):
1.36 + if s:
1.37 + return s.encode(encoding)
1.38 + else:
1.39 + return s
1.40 +
1.41 +from imiptools.period import get_overlapping, Period, PeriodBase
1.42 +
1.43 +class FreeBusyPeriod(PeriodBase):
1.44 +
1.45 + "A free/busy record abstraction."
1.46 +
1.47 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
1.48 + summary=None, organiser=None):
1.49 +
1.50 + """
1.51 + Initialise a free/busy period with the given 'start' and 'end' points,
1.52 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
1.53 + details.
1.54 + """
1.55 +
1.56 + PeriodBase.__init__(self, start, end)
1.57 + self.uid = uid
1.58 + self.transp = transp or None
1.59 + self.recurrenceid = recurrenceid or None
1.60 + self.summary = summary or None
1.61 + self.organiser = organiser or None
1.62 +
1.63 + def as_tuple(self, strings_only=False, string_datetimes=False):
1.64 +
1.65 + """
1.66 + Return the initialisation parameter tuple, converting datetimes and
1.67 + false value parameters to strings if 'strings_only' is set to a true
1.68 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
1.69 + datetime values are converted to strings.
1.70 + """
1.71 +
1.72 + null = lambda x: (strings_only and [""] or [x])[0]
1.73 + return (
1.74 + (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
1.75 + (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
1.76 + self.uid or null(self.uid),
1.77 + self.transp or strings_only and "OPAQUE" or None,
1.78 + self.recurrenceid or null(self.recurrenceid),
1.79 + self.summary or null(self.summary),
1.80 + self.organiser or null(self.organiser)
1.81 + )
1.82 +
1.83 + def __cmp__(self, other):
1.84 +
1.85 + """
1.86 + Compare this object to 'other', employing the uid if the periods
1.87 + involved are the same.
1.88 + """
1.89 +
1.90 + result = PeriodBase.__cmp__(self, other)
1.91 + if result == 0 and isinstance(other, FreeBusyPeriod):
1.92 + return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))
1.93 + else:
1.94 + return result
1.95 +
1.96 + def get_key(self):
1.97 + return self.uid, self.recurrenceid, self.get_start()
1.98 +
1.99 + def __repr__(self):
1.100 + return "FreeBusyPeriod%r" % (self.as_tuple(),)
1.101 +
1.102 + def get_tzid(self):
1.103 + return "UTC"
1.104 +
1.105 + # Period and event recurrence logic.
1.106 +
1.107 + def is_replaced(self, recurrences):
1.108 +
1.109 + """
1.110 + Return whether this period refers to one of the 'recurrences'.
1.111 + The 'recurrences' must be UTC datetimes corresponding to the start of
1.112 + the period described by a recurrence.
1.113 + """
1.114 +
1.115 + for recurrence in recurrences:
1.116 + if self.is_affected(recurrence):
1.117 + return True
1.118 + return False
1.119 +
1.120 + def is_affected(self, recurrence):
1.121 +
1.122 + """
1.123 + Return whether this period refers to 'recurrence'. The 'recurrence' must
1.124 + be a UTC datetime corresponding to the start of the period described by
1.125 + a recurrence.
1.126 + """
1.127 +
1.128 + return recurrence and self.get_start_point() == recurrence
1.129 +
1.130 + # Value correction methods.
1.131 +
1.132 + def make_corrected(self, start, end):
1.133 + return self.__class__(start, end)
1.134 +
1.135 +class FreeBusyOfferPeriod(FreeBusyPeriod):
1.136 +
1.137 + "A free/busy record abstraction for an offer period."
1.138 +
1.139 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
1.140 + summary=None, organiser=None, expires=None):
1.141 +
1.142 + """
1.143 + Initialise a free/busy period with the given 'start' and 'end' points,
1.144 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
1.145 + details.
1.146 +
1.147 + An additional 'expires' parameter can be used to indicate an expiry
1.148 + datetime in conjunction with free/busy offers made when countering
1.149 + event proposals.
1.150 + """
1.151 +
1.152 + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
1.153 + summary, organiser)
1.154 + self.expires = expires or None
1.155 +
1.156 + def as_tuple(self, strings_only=False, string_datetimes=False):
1.157 +
1.158 + """
1.159 + Return the initialisation parameter tuple, converting datetimes and
1.160 + false value parameters to strings if 'strings_only' is set to a true
1.161 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
1.162 + datetime values are converted to strings.
1.163 + """
1.164 +
1.165 + null = lambda x: (strings_only and [""] or [x])[0]
1.166 + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
1.167 + self.expires or null(self.expires),)
1.168 +
1.169 + def __repr__(self):
1.170 + return "FreeBusyOfferPeriod%r" % (self.as_tuple(),)
1.171 +
1.172 +class FreeBusyGroupPeriod(FreeBusyPeriod):
1.173 +
1.174 + "A free/busy record abstraction for a quota group period."
1.175 +
1.176 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
1.177 + summary=None, organiser=None, attendee=None):
1.178 +
1.179 + """
1.180 + Initialise a free/busy period with the given 'start' and 'end' points,
1.181 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
1.182 + details.
1.183 +
1.184 + An additional 'attendee' parameter can be used to indicate the identity
1.185 + of the attendee recording the period.
1.186 + """
1.187 +
1.188 + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
1.189 + summary, organiser)
1.190 + self.attendee = attendee or None
1.191 +
1.192 + def as_tuple(self, strings_only=False, string_datetimes=False):
1.193 +
1.194 + """
1.195 + Return the initialisation parameter tuple, converting datetimes and
1.196 + false value parameters to strings if 'strings_only' is set to a true
1.197 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
1.198 + datetime values are converted to strings.
1.199 + """
1.200 +
1.201 + null = lambda x: (strings_only and [""] or [x])[0]
1.202 + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
1.203 + self.attendee or null(self.attendee),)
1.204 +
1.205 + def __cmp__(self, other):
1.206 +
1.207 + """
1.208 + Compare this object to 'other', employing the uid if the periods
1.209 + involved are the same.
1.210 + """
1.211 +
1.212 + result = FreeBusyPeriod.__cmp__(self, other)
1.213 + if isinstance(other, FreeBusyGroupPeriod) and result == 0:
1.214 + return cmp(self.attendee, other.attendee)
1.215 + else:
1.216 + return result
1.217 +
1.218 + def __repr__(self):
1.219 + return "FreeBusyGroupPeriod%r" % (self.as_tuple(),)
1.220 +
1.221 +class FreeBusyCollectionBase:
1.222 +
1.223 + "Common operations on free/busy period collections."
1.224 +
1.225 + period_columns = [
1.226 + "start", "end", "object_uid", "transp", "object_recurrenceid",
1.227 + "summary", "organiser"
1.228 + ]
1.229 +
1.230 + period_class = FreeBusyPeriod
1.231 +
1.232 + def __init__(self, mutable=True):
1.233 + self.mutable = mutable
1.234 +
1.235 + def _check_mutable(self):
1.236 + if not self.mutable:
1.237 + raise TypeError, "Cannot mutate this collection."
1.238 +
1.239 + def copy(self):
1.240 +
1.241 + "Make an independent mutable copy of the collection."
1.242 +
1.243 + return FreeBusyCollection(list(self), True)
1.244 +
1.245 + def make_period(self, t):
1.246 +
1.247 + """
1.248 + Make a period using the given tuple of arguments and the collection's
1.249 + column details.
1.250 + """
1.251 +
1.252 + args = []
1.253 + for arg, column in zip(t, self.period_columns):
1.254 + args.append(from_string(arg, "utf-8"))
1.255 + return self.period_class(*args)
1.256 +
1.257 + def make_tuple(self, t):
1.258 +
1.259 + """
1.260 + Return a tuple from the given tuple 't' conforming to the collection's
1.261 + column details.
1.262 + """
1.263 +
1.264 + args = []
1.265 + for arg, column in zip(t, self.period_columns):
1.266 + args.append(arg)
1.267 + return tuple(args)
1.268 +
1.269 + # List emulation methods.
1.270 +
1.271 + def __iadd__(self, periods):
1.272 + for period in periods:
1.273 + self.insert_period(period)
1.274 + return self
1.275 +
1.276 + def append(self, period):
1.277 + self.insert_period(period)
1.278 +
1.279 + # Operations.
1.280 +
1.281 + def can_schedule(self, periods, uid, recurrenceid):
1.282 +
1.283 + """
1.284 + Return whether the collection can accommodate the given 'periods'
1.285 + employing the specified 'uid' and 'recurrenceid'.
1.286 + """
1.287 +
1.288 + for conflict in self.have_conflict(periods, True):
1.289 + if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
1.290 + return False
1.291 +
1.292 + return True
1.293 +
1.294 + def have_conflict(self, periods, get_conflicts=False):
1.295 +
1.296 + """
1.297 + Return whether any period in the collection overlaps with the given
1.298 + 'periods', returning a collection of such overlapping periods if
1.299 + 'get_conflicts' is set to a true value.
1.300 + """
1.301 +
1.302 + conflicts = set()
1.303 + for p in periods:
1.304 + overlapping = self.period_overlaps(p, get_conflicts)
1.305 + if overlapping:
1.306 + if get_conflicts:
1.307 + conflicts.update(overlapping)
1.308 + else:
1.309 + return True
1.310 +
1.311 + if get_conflicts:
1.312 + return conflicts
1.313 + else:
1.314 + return False
1.315 +
1.316 + def period_overlaps(self, period, get_periods=False):
1.317 +
1.318 + """
1.319 + Return whether any period in the collection overlaps with the given
1.320 + 'period', returning a collection of overlapping periods if 'get_periods'
1.321 + is set to a true value.
1.322 + """
1.323 +
1.324 + overlapping = self.get_overlapping([period])
1.325 +
1.326 + if get_periods:
1.327 + return overlapping
1.328 + else:
1.329 + return len(overlapping) != 0
1.330 +
1.331 + def replace_overlapping(self, period, replacements):
1.332 +
1.333 + """
1.334 + Replace existing periods in the collection within the given 'period',
1.335 + using the given 'replacements'.
1.336 + """
1.337 +
1.338 + self._check_mutable()
1.339 +
1.340 + self.remove_overlapping(period)
1.341 + for replacement in replacements:
1.342 + self.insert_period(replacement)
1.343 +
1.344 + def coalesce_freebusy(self):
1.345 +
1.346 + "Coalesce the periods in the collection, returning a new collection."
1.347 +
1.348 + if not self:
1.349 + return FreeBusyCollection()
1.350 +
1.351 + fb = []
1.352 +
1.353 + it = iter(self)
1.354 + period = it.next()
1.355 +
1.356 + start = period.get_start_point()
1.357 + end = period.get_end_point()
1.358 +
1.359 + try:
1.360 + while True:
1.361 + period = it.next()
1.362 + if period.get_start_point() > end:
1.363 + fb.append(self.period_class(start, end))
1.364 + start = period.get_start_point()
1.365 + end = period.get_end_point()
1.366 + else:
1.367 + end = max(end, period.get_end_point())
1.368 + except StopIteration:
1.369 + pass
1.370 +
1.371 + fb.append(self.period_class(start, end))
1.372 + return FreeBusyCollection(fb)
1.373 +
1.374 + def invert_freebusy(self):
1.375 +
1.376 + "Return the free periods from the collection as a new collection."
1.377 +
1.378 + if not self:
1.379 + return FreeBusyCollection([self.period_class(None, None)])
1.380 +
1.381 + # Coalesce periods that overlap or are adjacent.
1.382 +
1.383 + fb = self.coalesce_freebusy()
1.384 + free = []
1.385 +
1.386 + # Add a start-of-time period if appropriate.
1.387 +
1.388 + first = fb[0].get_start_point()
1.389 + if first:
1.390 + free.append(self.period_class(None, first))
1.391 +
1.392 + start = fb[0].get_end_point()
1.393 +
1.394 + for period in fb[1:]:
1.395 + free.append(self.period_class(start, period.get_start_point()))
1.396 + start = period.get_end_point()
1.397 +
1.398 + # Add an end-of-time period if appropriate.
1.399 +
1.400 + if start:
1.401 + free.append(self.period_class(start, None))
1.402 +
1.403 + return FreeBusyCollection(free)
1.404 +
1.405 + def _update_freebusy(self, periods, uid, recurrenceid):
1.406 +
1.407 + """
1.408 + Update the free/busy details with the given 'periods', using the given
1.409 + 'uid' plus 'recurrenceid' to remove existing periods.
1.410 + """
1.411 +
1.412 + self._check_mutable()
1.413 +
1.414 + self.remove_specific_event_periods(uid, recurrenceid)
1.415 +
1.416 + for p in periods:
1.417 + self.insert_period(p)
1.418 +
1.419 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser):
1.420 +
1.421 + """
1.422 + Update the free/busy details with the given 'periods', 'transp' setting,
1.423 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.424 + """
1.425 +
1.426 + new_periods = []
1.427 +
1.428 + for p in periods:
1.429 + new_periods.append(
1.430 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser)
1.431 + )
1.432 +
1.433 + self._update_freebusy(new_periods, uid, recurrenceid)
1.434 +
1.435 +class SupportAttendee:
1.436 +
1.437 + "A mix-in that supports the affected attendee in free/busy periods."
1.438 +
1.439 + period_columns = FreeBusyCollectionBase.period_columns + ["attendee"]
1.440 + period_class = FreeBusyGroupPeriod
1.441 +
1.442 + def _update_freebusy(self, periods, uid, recurrenceid, attendee=None):
1.443 +
1.444 + """
1.445 + Update the free/busy details with the given 'periods', using the given
1.446 + 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods.
1.447 + """
1.448 +
1.449 + self._check_mutable()
1.450 +
1.451 + self.remove_specific_event_periods(uid, recurrenceid, attendee)
1.452 +
1.453 + for p in periods:
1.454 + self.insert_period(p)
1.455 +
1.456 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None):
1.457 +
1.458 + """
1.459 + Update the free/busy details with the given 'periods', 'transp' setting,
1.460 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.461 +
1.462 + An optional 'attendee' indicates the attendee affected by the period.
1.463 + """
1.464 +
1.465 + new_periods = []
1.466 +
1.467 + for p in periods:
1.468 + new_periods.append(
1.469 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee)
1.470 + )
1.471 +
1.472 + self._update_freebusy(new_periods, uid, recurrenceid, attendee)
1.473 +
1.474 +class SupportExpires:
1.475 +
1.476 + "A mix-in that supports the expiry datetime in free/busy periods."
1.477 +
1.478 + period_columns = FreeBusyCollectionBase.period_columns + ["expires"]
1.479 + period_class = FreeBusyOfferPeriod
1.480 +
1.481 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
1.482 +
1.483 + """
1.484 + Update the free/busy details with the given 'periods', 'transp' setting,
1.485 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.486 +
1.487 + An optional 'expires' datetime string indicates the expiry time of any
1.488 + free/busy offer.
1.489 + """
1.490 +
1.491 + new_periods = []
1.492 +
1.493 + for p in periods:
1.494 + new_periods.append(
1.495 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)
1.496 + )
1.497 +
1.498 + self._update_freebusy(new_periods, uid, recurrenceid)
1.499 +
1.500 +class FreeBusyCollection(FreeBusyCollectionBase):
1.501 +
1.502 + "An abstraction for a collection of free/busy periods."
1.503 +
1.504 + def __init__(self, periods=None, mutable=True):
1.505 +
1.506 + """
1.507 + Initialise the collection with the given list of 'periods', or start an
1.508 + empty collection if no list is given. If 'mutable' is indicated, the
1.509 + collection may be changed; otherwise, an exception will be raised.
1.510 + """
1.511 +
1.512 + FreeBusyCollectionBase.__init__(self, mutable)
1.513 + self.periods = periods or []
1.514 +
1.515 + # List emulation methods.
1.516 +
1.517 + def __nonzero__(self):
1.518 + return bool(self.periods)
1.519 +
1.520 + def __iter__(self):
1.521 + return iter(self.periods)
1.522 +
1.523 + def __len__(self):
1.524 + return len(self.periods)
1.525 +
1.526 + def __getitem__(self, i):
1.527 + return self.periods[i]
1.528 +
1.529 + # Operations.
1.530 +
1.531 + def insert_period(self, period):
1.532 +
1.533 + "Insert the given 'period' into the collection."
1.534 +
1.535 + self._check_mutable()
1.536 +
1.537 + i = bisect_left(self.periods, period)
1.538 + if i == len(self.periods):
1.539 + self.periods.append(period)
1.540 + elif self.periods[i] != period:
1.541 + self.periods.insert(i, period)
1.542 +
1.543 + def remove_periods(self, periods):
1.544 +
1.545 + "Remove the given 'periods' from the collection."
1.546 +
1.547 + self._check_mutable()
1.548 +
1.549 + for period in periods:
1.550 + i = bisect_left(self.periods, period)
1.551 + if i < len(self.periods) and self.periods[i] == period:
1.552 + del self.periods[i]
1.553 +
1.554 + def remove_event_periods(self, uid, recurrenceid=None):
1.555 +
1.556 + """
1.557 + Remove from the collection all periods associated with 'uid' and
1.558 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.559 + be referenced).
1.560 +
1.561 + Return the removed periods.
1.562 + """
1.563 +
1.564 + self._check_mutable()
1.565 +
1.566 + removed = []
1.567 + i = 0
1.568 + while i < len(self.periods):
1.569 + fb = self.periods[i]
1.570 + if fb.uid == uid and fb.recurrenceid == recurrenceid:
1.571 + removed.append(self.periods[i])
1.572 + del self.periods[i]
1.573 + else:
1.574 + i += 1
1.575 +
1.576 + return removed
1.577 +
1.578 + # Specific period removal when updating event details.
1.579 +
1.580 + remove_specific_event_periods = remove_event_periods
1.581 +
1.582 + def remove_additional_periods(self, uid, recurrenceids=None):
1.583 +
1.584 + """
1.585 + Remove from the collection all periods associated with 'uid' having a
1.586 + recurrence identifier indicating an additional or modified period.
1.587 +
1.588 + If 'recurrenceids' is specified, remove all periods associated with
1.589 + 'uid' that do not have a recurrence identifier in the given list.
1.590 +
1.591 + Return the removed periods.
1.592 + """
1.593 +
1.594 + self._check_mutable()
1.595 +
1.596 + removed = []
1.597 + i = 0
1.598 + while i < len(self.periods):
1.599 + fb = self.periods[i]
1.600 + if fb.uid == uid and fb.recurrenceid and (
1.601 + recurrenceids is None or
1.602 + recurrenceids is not None and fb.recurrenceid not in recurrenceids
1.603 + ):
1.604 + removed.append(self.periods[i])
1.605 + del self.periods[i]
1.606 + else:
1.607 + i += 1
1.608 +
1.609 + return removed
1.610 +
1.611 + def remove_affected_period(self, uid, start):
1.612 +
1.613 + """
1.614 + Remove from the collection the period associated with 'uid' that
1.615 + provides an occurrence starting at the given 'start' (provided by a
1.616 + recurrence identifier, converted to a datetime). A recurrence identifier
1.617 + is used to provide an alternative time period whilst also acting as a
1.618 + reference to the originally-defined occurrence.
1.619 +
1.620 + Return any removed period in a list.
1.621 + """
1.622 +
1.623 + self._check_mutable()
1.624 +
1.625 + removed = []
1.626 +
1.627 + search = Period(start, start)
1.628 + found = bisect_left(self.periods, search)
1.629 +
1.630 + while found < len(self.periods):
1.631 + fb = self.periods[found]
1.632 +
1.633 + # Stop looking if the start no longer matches the recurrence identifier.
1.634 +
1.635 + if fb.get_start_point() != search.get_start_point():
1.636 + break
1.637 +
1.638 + # If the period belongs to the parent object, remove it and return.
1.639 +
1.640 + if not fb.recurrenceid and uid == fb.uid:
1.641 + removed.append(self.periods[found])
1.642 + del self.periods[found]
1.643 + break
1.644 +
1.645 + # Otherwise, keep looking for a matching period.
1.646 +
1.647 + found += 1
1.648 +
1.649 + return removed
1.650 +
1.651 + def periods_from(self, period):
1.652 +
1.653 + "Return the entries in the collection at or after 'period'."
1.654 +
1.655 + first = bisect_left(self.periods, period)
1.656 + return self.periods[first:]
1.657 +
1.658 + def periods_until(self, period):
1.659 +
1.660 + "Return the entries in the collection before 'period'."
1.661 +
1.662 + last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
1.663 + return self.periods[:last]
1.664 +
1.665 + def get_overlapping(self, periods):
1.666 +
1.667 + """
1.668 + Return the entries in the collection providing periods overlapping with
1.669 + the given sorted collection of 'periods'.
1.670 + """
1.671 +
1.672 + return get_overlapping(self.periods, periods)
1.673 +
1.674 + def remove_overlapping(self, period):
1.675 +
1.676 + "Remove all periods overlapping with 'period' from the collection."
1.677 +
1.678 + self._check_mutable()
1.679 +
1.680 + overlapping = self.get_overlapping([period])
1.681 +
1.682 + if overlapping:
1.683 + for fb in overlapping:
1.684 + self.periods.remove(fb)
1.685 +
1.686 +class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection):
1.687 +
1.688 + "A collection of quota group free/busy objects."
1.689 +
1.690 + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
1.691 +
1.692 + """
1.693 + Remove from the collection all periods associated with 'uid' and
1.694 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.695 + be referenced) and any 'attendee'.
1.696 +
1.697 + Return the removed periods.
1.698 + """
1.699 +
1.700 + self._check_mutable()
1.701 +
1.702 + removed = []
1.703 + i = 0
1.704 + while i < len(self.periods):
1.705 + fb = self.periods[i]
1.706 + if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee:
1.707 + removed.append(self.periods[i])
1.708 + del self.periods[i]
1.709 + else:
1.710 + i += 1
1.711 +
1.712 + return removed
1.713 +
1.714 +class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection):
1.715 +
1.716 + "A collection of offered free/busy objects."
1.717 +
1.718 + pass
1.719 +
1.720 +class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations):
1.721 +
1.722 + """
1.723 + An abstraction for a collection of free/busy periods stored in a database
1.724 + system.
1.725 + """
1.726 +
1.727 + def __init__(self, cursor, table_name, column_names=None, filter_values=None,
1.728 + mutable=True, paramstyle=None):
1.729 +
1.730 + """
1.731 + Initialise the collection with the given 'cursor' and with the
1.732 + 'table_name', 'column_names' and 'filter_values' configuring the
1.733 + selection of data. If 'mutable' is indicated, the collection may be
1.734 + changed; otherwise, an exception will be raised.
1.735 + """
1.736 +
1.737 + FreeBusyCollectionBase.__init__(self, mutable)
1.738 + DatabaseOperations.__init__(self, column_names, filter_values, paramstyle)
1.739 + self.cursor = cursor
1.740 + self.table_name = table_name
1.741 +
1.742 + # List emulation methods.
1.743 +
1.744 + def __nonzero__(self):
1.745 + return len(self) and True or False
1.746 +
1.747 + def __iter__(self):
1.748 + query, values = self.get_query(
1.749 + "select %(columns)s from %(table)s :condition" % {
1.750 + "columns" : self.columnlist(self.period_columns),
1.751 + "table" : self.table_name
1.752 + })
1.753 + self.cursor.execute(query, values)
1.754 + return iter(map(lambda t: self.make_period(t), self.cursor.fetchall()))
1.755 +
1.756 + def __len__(self):
1.757 + query, values = self.get_query(
1.758 + "select count(*) from %(table)s :condition" % {
1.759 + "table" : self.table_name
1.760 + })
1.761 + self.cursor.execute(query, values)
1.762 + result = self.cursor.fetchone()
1.763 + return result and int(result[0]) or 0
1.764 +
1.765 + def __getitem__(self, i):
1.766 + return list(iter(self))[i]
1.767 +
1.768 + # Operations.
1.769 +
1.770 + def insert_period(self, period):
1.771 +
1.772 + "Insert the given 'period' into the collection."
1.773 +
1.774 + self._check_mutable()
1.775 +
1.776 + columns, values = self.period_columns, period.as_tuple(string_datetimes=True)
1.777 +
1.778 + query, values = self.get_query(
1.779 + "insert into %(table)s (:columns) values (:values)" % {
1.780 + "table" : self.table_name
1.781 + },
1.782 + columns, [to_string(v, "utf-8") for v in values])
1.783 +
1.784 + self.cursor.execute(query, values)
1.785 +
1.786 + def remove_periods(self, periods):
1.787 +
1.788 + "Remove the given 'periods' from the collection."
1.789 +
1.790 + self._check_mutable()
1.791 +
1.792 + for period in periods:
1.793 + values = period.as_tuple(string_datetimes=True)
1.794 +
1.795 + query, values = self.get_query(
1.796 + "delete from %(table)s :condition" % {
1.797 + "table" : self.table_name
1.798 + },
1.799 + self.period_columns, [to_string(v, "utf-8") for v in values])
1.800 +
1.801 + self.cursor.execute(query, values)
1.802 +
1.803 + def remove_event_periods(self, uid, recurrenceid=None):
1.804 +
1.805 + """
1.806 + Remove from the collection all periods associated with 'uid' and
1.807 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.808 + be referenced).
1.809 +
1.810 + Return the removed periods.
1.811 + """
1.812 +
1.813 + self._check_mutable()
1.814 +
1.815 + if recurrenceid:
1.816 + columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid]
1.817 + else:
1.818 + columns, values = ["object_uid", "object_recurrenceid is null"], [uid]
1.819 +
1.820 + query, _values = self.get_query(
1.821 + "select %(columns)s from %(table)s :condition" % {
1.822 + "columns" : self.columnlist(self.period_columns),
1.823 + "table" : self.table_name
1.824 + },
1.825 + columns, values)
1.826 +
1.827 + self.cursor.execute(query, _values)
1.828 + removed = self.cursor.fetchall()
1.829 +
1.830 + query, values = self.get_query(
1.831 + "delete from %(table)s :condition" % {
1.832 + "table" : self.table_name
1.833 + },
1.834 + columns, values)
1.835 +
1.836 + self.cursor.execute(query, values)
1.837 +
1.838 + return map(lambda t: self.make_period(t), removed)
1.839 +
1.840 + # Specific period removal when updating event details.
1.841 +
1.842 + remove_specific_event_periods = remove_event_periods
1.843 +
1.844 + def remove_additional_periods(self, uid, recurrenceids=None):
1.845 +
1.846 + """
1.847 + Remove from the collection all periods associated with 'uid' having a
1.848 + recurrence identifier indicating an additional or modified period.
1.849 +
1.850 + If 'recurrenceids' is specified, remove all periods associated with
1.851 + 'uid' that do not have a recurrence identifier in the given list.
1.852 +
1.853 + Return the removed periods.
1.854 + """
1.855 +
1.856 + self._check_mutable()
1.857 +
1.858 + if not recurrenceids:
1.859 + columns, values = ["object_uid", "object_recurrenceid is not null"], [uid]
1.860 + else:
1.861 + columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)]
1.862 +
1.863 + query, _values = self.get_query(
1.864 + "select %(columns)s from %(table)s :condition" % {
1.865 + "columns" : self.columnlist(self.period_columns),
1.866 + "table" : self.table_name
1.867 + },
1.868 + columns, values)
1.869 +
1.870 + self.cursor.execute(query, _values)
1.871 + removed = self.cursor.fetchall()
1.872 +
1.873 + query, values = self.get_query(
1.874 + "delete from %(table)s :condition" % {
1.875 + "table" : self.table_name
1.876 + },
1.877 + columns, values)
1.878 +
1.879 + self.cursor.execute(query, values)
1.880 +
1.881 + return map(lambda t: self.make_period(t), removed)
1.882 +
1.883 + def remove_affected_period(self, uid, start):
1.884 +
1.885 + """
1.886 + Remove from the collection the period associated with 'uid' that
1.887 + provides an occurrence starting at the given 'start' (provided by a
1.888 + recurrence identifier, converted to a datetime). A recurrence identifier
1.889 + is used to provide an alternative time period whilst also acting as a
1.890 + reference to the originally-defined occurrence.
1.891 +
1.892 + Return any removed period in a list.
1.893 + """
1.894 +
1.895 + self._check_mutable()
1.896 +
1.897 + start = format_datetime(start)
1.898 +
1.899 + columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start]
1.900 +
1.901 + query, _values = self.get_query(
1.902 + "select %(columns)s from %(table)s :condition" % {
1.903 + "columns" : self.columnlist(self.period_columns),
1.904 + "table" : self.table_name
1.905 + },
1.906 + columns, values)
1.907 +
1.908 + self.cursor.execute(query, _values)
1.909 + removed = self.cursor.fetchall()
1.910 +
1.911 + query, values = self.get_query(
1.912 + "delete from %(table)s :condition" % {
1.913 + "table" : self.table_name
1.914 + },
1.915 + columns, values)
1.916 +
1.917 + self.cursor.execute(query, values)
1.918 +
1.919 + return map(lambda t: self.make_period(t), removed)
1.920 +
1.921 + def periods_from(self, period):
1.922 +
1.923 + "Return the entries in the collection at or after 'period'."
1.924 +
1.925 + start = format_datetime(period.get_start_point())
1.926 +
1.927 + columns, values = [], []
1.928 +
1.929 + if start:
1.930 + columns.append("start >= ?")
1.931 + values.append(start)
1.932 +
1.933 + query, values = self.get_query(
1.934 + "select %(columns)s from %(table)s :condition" % {
1.935 + "columns" : self.columnlist(self.period_columns),
1.936 + "table" : self.table_name
1.937 + },
1.938 + columns, values)
1.939 +
1.940 + self.cursor.execute(query, values)
1.941 +
1.942 + return map(lambda t: self.make_period(t), self.cursor.fetchall())
1.943 +
1.944 + def periods_until(self, period):
1.945 +
1.946 + "Return the entries in the collection before 'period'."
1.947 +
1.948 + end = format_datetime(period.get_end_point())
1.949 +
1.950 + columns, values = [], []
1.951 +
1.952 + if end:
1.953 + columns.append("start < ?")
1.954 + values.append(end)
1.955 +
1.956 + query, values = self.get_query(
1.957 + "select %(columns)s from %(table)s :condition" % {
1.958 + "columns" : self.columnlist(self.period_columns),
1.959 + "table" : self.table_name
1.960 + },
1.961 + columns, values)
1.962 +
1.963 + self.cursor.execute(query, values)
1.964 +
1.965 + return map(lambda t: self.make_period(t), self.cursor.fetchall())
1.966 +
1.967 + def get_overlapping(self, periods):
1.968 +
1.969 + """
1.970 + Return the entries in the collection providing periods overlapping with
1.971 + the given sorted collection of 'periods'.
1.972 + """
1.973 +
1.974 + overlapping = set()
1.975 +
1.976 + for period in periods:
1.977 + columns, values = self._get_period_values(period)
1.978 +
1.979 + query, values = self.get_query(
1.980 + "select %(columns)s from %(table)s :condition" % {
1.981 + "columns" : self.columnlist(self.period_columns),
1.982 + "table" : self.table_name
1.983 + },
1.984 + columns, values)
1.985 +
1.986 + self.cursor.execute(query, values)
1.987 +
1.988 + overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall()))
1.989 +
1.990 + overlapping = list(overlapping)
1.991 + overlapping.sort()
1.992 + return overlapping
1.993 +
1.994 + def remove_overlapping(self, period):
1.995 +
1.996 + "Remove all periods overlapping with 'period' from the collection."
1.997 +
1.998 + self._check_mutable()
1.999 +
1.1000 + columns, values = self._get_period_values(period)
1.1001 +
1.1002 + query, values = self.get_query(
1.1003 + "delete from %(table)s :condition" % {
1.1004 + "table" : self.table_name
1.1005 + },
1.1006 + columns, values)
1.1007 +
1.1008 + self.cursor.execute(query, values)
1.1009 +
1.1010 + def _get_period_values(self, period):
1.1011 +
1.1012 + start = format_datetime(period.get_start_point())
1.1013 + end = format_datetime(period.get_end_point())
1.1014 +
1.1015 + columns, values = [], []
1.1016 +
1.1017 + if end:
1.1018 + columns.append("start < ?")
1.1019 + values.append(end)
1.1020 + if start:
1.1021 + columns.append("end > ?")
1.1022 + values.append(start)
1.1023 +
1.1024 + return columns, values
1.1025 +
1.1026 +class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection):
1.1027 +
1.1028 + "A collection of quota group free/busy objects."
1.1029 +
1.1030 + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
1.1031 +
1.1032 + """
1.1033 + Remove from the collection all periods associated with 'uid' and
1.1034 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.1035 + be referenced) and any 'attendee'.
1.1036 +
1.1037 + Return the removed periods.
1.1038 + """
1.1039 +
1.1040 + self._check_mutable()
1.1041 +
1.1042 + columns, values = ["object_uid"], [uid]
1.1043 +
1.1044 + if recurrenceid:
1.1045 + columns.append("object_recurrenceid")
1.1046 + values.append(recurrenceid)
1.1047 + else:
1.1048 + columns.append("object_recurrenceid is null")
1.1049 +
1.1050 + if attendee:
1.1051 + columns.append("attendee")
1.1052 + values.append(attendee)
1.1053 + else:
1.1054 + columns.append("attendee is null")
1.1055 +
1.1056 + query, _values = self.get_query(
1.1057 + "select %(columns)s from %(table)s :condition" % {
1.1058 + "columns" : self.columnlist(self.period_columns),
1.1059 + "table" : self.table_name
1.1060 + },
1.1061 + columns, values)
1.1062 +
1.1063 + self.cursor.execute(query, _values)
1.1064 + removed = self.cursor.fetchall()
1.1065 +
1.1066 + query, values = self.get_query(
1.1067 + "delete from %(table)s :condition" % {
1.1068 + "table" : self.table_name
1.1069 + },
1.1070 + columns, values)
1.1071 +
1.1072 + self.cursor.execute(query, values)
1.1073 +
1.1074 + return map(lambda t: self.make_period(t), removed)
1.1075 +
1.1076 +class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection):
1.1077 +
1.1078 + "A collection of offered free/busy objects."
1.1079 +
1.1080 + pass
1.1081 +
1.1082 +# vim: tabstop=4 expandtab shiftwidth=4