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