1.1 --- a/imiptools/client.py Tue May 23 16:31:27 2017 +0200
1.2 +++ b/imiptools/client.py Tue May 23 16:34:09 2017 +0200
1.3 @@ -28,7 +28,7 @@
1.4 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
1.5 get_duration, get_timestamp
1.6 from imiptools.i18n import get_translator
1.7 -from imiptools.period import SupportAttendee, SupportExpires
1.8 +from imiptools.freebusy import SupportAttendee, SupportExpires
1.9 from imiptools.profile import Preferences
1.10 from imiptools.stores import get_store, get_publisher, get_journal
1.11
2.1 --- a/imiptools/data.py Tue May 23 16:31:27 2017 +0200
2.2 +++ b/imiptools/data.py Tue May 23 16:34:09 2017 +0200
2.3 @@ -3,7 +3,7 @@
2.4 """
2.5 Interpretation of vCalendar content.
2.6
2.7 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
2.8 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
2.9
2.10 This program is free software; you can redistribute it and/or modify it under
2.11 the terms of the GNU General Public License as published by the Free Software
2.12 @@ -29,7 +29,8 @@
2.13 get_recurrence_start_point, \
2.14 get_time, get_timestamp, get_tzid, to_datetime, \
2.15 to_timezone, to_utc_datetime
2.16 -from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod
2.17 +from imiptools.freebusy import FreeBusyPeriod
2.18 +from imiptools.period import Period, RecurringPeriod
2.19 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
2.20 from vRecurrence import get_parameters, get_rule
2.21 import email.utils
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/imiptools/freebusy.py Tue May 23 16:34:09 2017 +0200
3.3 @@ -0,0 +1,1079 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +Managing free/busy periods.
3.8 +
3.9 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
3.10 +
3.11 +This program is free software; you can redistribute it and/or modify it under
3.12 +the terms of the GNU General Public License as published by the Free Software
3.13 +Foundation; either version 3 of the License, or (at your option) any later
3.14 +version.
3.15 +
3.16 +This program is distributed in the hope that it will be useful, but WITHOUT
3.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3.19 +details.
3.20 +
3.21 +You should have received a copy of the GNU General Public License along with
3.22 +this program. If not, see <http://www.gnu.org/licenses/>.
3.23 +"""
3.24 +
3.25 +from bisect import bisect_left, bisect_right
3.26 +from imiptools.dates import format_datetime
3.27 +from imiptools.sql import DatabaseOperations
3.28 +
3.29 +def from_string(s, encoding):
3.30 + if s:
3.31 + return unicode(s, encoding)
3.32 + else:
3.33 + return s
3.34 +
3.35 +def to_string(s, encoding):
3.36 + if s:
3.37 + return s.encode(encoding)
3.38 + else:
3.39 + return s
3.40 +
3.41 +from imiptools.period import get_overlapping, Period, PeriodBase
3.42 +
3.43 +class FreeBusyPeriod(PeriodBase):
3.44 +
3.45 + "A free/busy record abstraction."
3.46 +
3.47 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
3.48 + summary=None, organiser=None):
3.49 +
3.50 + """
3.51 + Initialise a free/busy period with the given 'start' and 'end' points,
3.52 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
3.53 + details.
3.54 + """
3.55 +
3.56 + PeriodBase.__init__(self, start, end)
3.57 + self.uid = uid
3.58 + self.transp = transp or None
3.59 + self.recurrenceid = recurrenceid or None
3.60 + self.summary = summary or None
3.61 + self.organiser = organiser or None
3.62 +
3.63 + def as_tuple(self, strings_only=False, string_datetimes=False):
3.64 +
3.65 + """
3.66 + Return the initialisation parameter tuple, converting datetimes and
3.67 + false value parameters to strings if 'strings_only' is set to a true
3.68 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
3.69 + datetime values are converted to strings.
3.70 + """
3.71 +
3.72 + null = lambda x: (strings_only and [""] or [x])[0]
3.73 + return (
3.74 + (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
3.75 + (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
3.76 + self.uid or null(self.uid),
3.77 + self.transp or strings_only and "OPAQUE" or None,
3.78 + self.recurrenceid or null(self.recurrenceid),
3.79 + self.summary or null(self.summary),
3.80 + self.organiser or null(self.organiser)
3.81 + )
3.82 +
3.83 + def __cmp__(self, other):
3.84 +
3.85 + """
3.86 + Compare this object to 'other', employing the uid if the periods
3.87 + involved are the same.
3.88 + """
3.89 +
3.90 + result = PeriodBase.__cmp__(self, other)
3.91 + if result == 0 and isinstance(other, FreeBusyPeriod):
3.92 + return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))
3.93 + else:
3.94 + return result
3.95 +
3.96 + def get_key(self):
3.97 + return self.uid, self.recurrenceid, self.get_start()
3.98 +
3.99 + def __repr__(self):
3.100 + return "FreeBusyPeriod%r" % (self.as_tuple(),)
3.101 +
3.102 + def get_tzid(self):
3.103 + return "UTC"
3.104 +
3.105 + # Period and event recurrence logic.
3.106 +
3.107 + def is_replaced(self, recurrences):
3.108 +
3.109 + """
3.110 + Return whether this period refers to one of the 'recurrences'.
3.111 + The 'recurrences' must be UTC datetimes corresponding to the start of
3.112 + the period described by a recurrence.
3.113 + """
3.114 +
3.115 + for recurrence in recurrences:
3.116 + if self.is_affected(recurrence):
3.117 + return True
3.118 + return False
3.119 +
3.120 + def is_affected(self, recurrence):
3.121 +
3.122 + """
3.123 + Return whether this period refers to 'recurrence'. The 'recurrence' must
3.124 + be a UTC datetime corresponding to the start of the period described by
3.125 + a recurrence.
3.126 + """
3.127 +
3.128 + return recurrence and self.get_start_point() == recurrence
3.129 +
3.130 + # Value correction methods.
3.131 +
3.132 + def make_corrected(self, start, end):
3.133 + return self.__class__(start, end)
3.134 +
3.135 +class FreeBusyOfferPeriod(FreeBusyPeriod):
3.136 +
3.137 + "A free/busy record abstraction for an offer period."
3.138 +
3.139 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
3.140 + summary=None, organiser=None, expires=None):
3.141 +
3.142 + """
3.143 + Initialise a free/busy period with the given 'start' and 'end' points,
3.144 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
3.145 + details.
3.146 +
3.147 + An additional 'expires' parameter can be used to indicate an expiry
3.148 + datetime in conjunction with free/busy offers made when countering
3.149 + event proposals.
3.150 + """
3.151 +
3.152 + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
3.153 + summary, organiser)
3.154 + self.expires = expires or None
3.155 +
3.156 + def as_tuple(self, strings_only=False, string_datetimes=False):
3.157 +
3.158 + """
3.159 + Return the initialisation parameter tuple, converting datetimes and
3.160 + false value parameters to strings if 'strings_only' is set to a true
3.161 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
3.162 + datetime values are converted to strings.
3.163 + """
3.164 +
3.165 + null = lambda x: (strings_only and [""] or [x])[0]
3.166 + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
3.167 + self.expires or null(self.expires),)
3.168 +
3.169 + def __repr__(self):
3.170 + return "FreeBusyOfferPeriod%r" % (self.as_tuple(),)
3.171 +
3.172 +class FreeBusyGroupPeriod(FreeBusyPeriod):
3.173 +
3.174 + "A free/busy record abstraction for a quota group period."
3.175 +
3.176 + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
3.177 + summary=None, organiser=None, attendee=None):
3.178 +
3.179 + """
3.180 + Initialise a free/busy period with the given 'start' and 'end' points,
3.181 + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
3.182 + details.
3.183 +
3.184 + An additional 'attendee' parameter can be used to indicate the identity
3.185 + of the attendee recording the period.
3.186 + """
3.187 +
3.188 + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
3.189 + summary, organiser)
3.190 + self.attendee = attendee or None
3.191 +
3.192 + def as_tuple(self, strings_only=False, string_datetimes=False):
3.193 +
3.194 + """
3.195 + Return the initialisation parameter tuple, converting datetimes and
3.196 + false value parameters to strings if 'strings_only' is set to a true
3.197 + value. Otherwise, if 'string_datetimes' is set to a true value, only the
3.198 + datetime values are converted to strings.
3.199 + """
3.200 +
3.201 + null = lambda x: (strings_only and [""] or [x])[0]
3.202 + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
3.203 + self.attendee or null(self.attendee),)
3.204 +
3.205 + def __cmp__(self, other):
3.206 +
3.207 + """
3.208 + Compare this object to 'other', employing the uid if the periods
3.209 + involved are the same.
3.210 + """
3.211 +
3.212 + result = FreeBusyPeriod.__cmp__(self, other)
3.213 + if isinstance(other, FreeBusyGroupPeriod) and result == 0:
3.214 + return cmp(self.attendee, other.attendee)
3.215 + else:
3.216 + return result
3.217 +
3.218 + def __repr__(self):
3.219 + return "FreeBusyGroupPeriod%r" % (self.as_tuple(),)
3.220 +
3.221 +class FreeBusyCollectionBase:
3.222 +
3.223 + "Common operations on free/busy period collections."
3.224 +
3.225 + period_columns = [
3.226 + "start", "end", "object_uid", "transp", "object_recurrenceid",
3.227 + "summary", "organiser"
3.228 + ]
3.229 +
3.230 + period_class = FreeBusyPeriod
3.231 +
3.232 + def __init__(self, mutable=True):
3.233 + self.mutable = mutable
3.234 +
3.235 + def _check_mutable(self):
3.236 + if not self.mutable:
3.237 + raise TypeError, "Cannot mutate this collection."
3.238 +
3.239 + def copy(self):
3.240 +
3.241 + "Make an independent mutable copy of the collection."
3.242 +
3.243 + return FreeBusyCollection(list(self), True)
3.244 +
3.245 + def make_period(self, t):
3.246 +
3.247 + """
3.248 + Make a period using the given tuple of arguments and the collection's
3.249 + column details.
3.250 + """
3.251 +
3.252 + args = []
3.253 + for arg, column in zip(t, self.period_columns):
3.254 + args.append(from_string(arg, "utf-8"))
3.255 + return self.period_class(*args)
3.256 +
3.257 + def make_tuple(self, t):
3.258 +
3.259 + """
3.260 + Return a tuple from the given tuple 't' conforming to the collection's
3.261 + column details.
3.262 + """
3.263 +
3.264 + args = []
3.265 + for arg, column in zip(t, self.period_columns):
3.266 + args.append(arg)
3.267 + return tuple(args)
3.268 +
3.269 + # List emulation methods.
3.270 +
3.271 + def __iadd__(self, periods):
3.272 + for period in periods:
3.273 + self.insert_period(period)
3.274 + return self
3.275 +
3.276 + def append(self, period):
3.277 + self.insert_period(period)
3.278 +
3.279 + # Operations.
3.280 +
3.281 + def can_schedule(self, periods, uid, recurrenceid):
3.282 +
3.283 + """
3.284 + Return whether the collection can accommodate the given 'periods'
3.285 + employing the specified 'uid' and 'recurrenceid'.
3.286 + """
3.287 +
3.288 + for conflict in self.have_conflict(periods, True):
3.289 + if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
3.290 + return False
3.291 +
3.292 + return True
3.293 +
3.294 + def have_conflict(self, periods, get_conflicts=False):
3.295 +
3.296 + """
3.297 + Return whether any period in the collection overlaps with the given
3.298 + 'periods', returning a collection of such overlapping periods if
3.299 + 'get_conflicts' is set to a true value.
3.300 + """
3.301 +
3.302 + conflicts = set()
3.303 + for p in periods:
3.304 + overlapping = self.period_overlaps(p, get_conflicts)
3.305 + if overlapping:
3.306 + if get_conflicts:
3.307 + conflicts.update(overlapping)
3.308 + else:
3.309 + return True
3.310 +
3.311 + if get_conflicts:
3.312 + return conflicts
3.313 + else:
3.314 + return False
3.315 +
3.316 + def period_overlaps(self, period, get_periods=False):
3.317 +
3.318 + """
3.319 + Return whether any period in the collection overlaps with the given
3.320 + 'period', returning a collection of overlapping periods if 'get_periods'
3.321 + is set to a true value.
3.322 + """
3.323 +
3.324 + overlapping = self.get_overlapping([period])
3.325 +
3.326 + if get_periods:
3.327 + return overlapping
3.328 + else:
3.329 + return len(overlapping) != 0
3.330 +
3.331 + def replace_overlapping(self, period, replacements):
3.332 +
3.333 + """
3.334 + Replace existing periods in the collection within the given 'period',
3.335 + using the given 'replacements'.
3.336 + """
3.337 +
3.338 + self._check_mutable()
3.339 +
3.340 + self.remove_overlapping(period)
3.341 + for replacement in replacements:
3.342 + self.insert_period(replacement)
3.343 +
3.344 + def coalesce_freebusy(self):
3.345 +
3.346 + "Coalesce the periods in the collection, returning a new collection."
3.347 +
3.348 + if not self:
3.349 + return FreeBusyCollection()
3.350 +
3.351 + fb = []
3.352 +
3.353 + it = iter(self)
3.354 + period = it.next()
3.355 +
3.356 + start = period.get_start_point()
3.357 + end = period.get_end_point()
3.358 +
3.359 + try:
3.360 + while True:
3.361 + period = it.next()
3.362 + if period.get_start_point() > end:
3.363 + fb.append(self.period_class(start, end))
3.364 + start = period.get_start_point()
3.365 + end = period.get_end_point()
3.366 + else:
3.367 + end = max(end, period.get_end_point())
3.368 + except StopIteration:
3.369 + pass
3.370 +
3.371 + fb.append(self.period_class(start, end))
3.372 + return FreeBusyCollection(fb)
3.373 +
3.374 + def invert_freebusy(self):
3.375 +
3.376 + "Return the free periods from the collection as a new collection."
3.377 +
3.378 + if not self:
3.379 + return FreeBusyCollection([self.period_class(None, None)])
3.380 +
3.381 + # Coalesce periods that overlap or are adjacent.
3.382 +
3.383 + fb = self.coalesce_freebusy()
3.384 + free = []
3.385 +
3.386 + # Add a start-of-time period if appropriate.
3.387 +
3.388 + first = fb[0].get_start_point()
3.389 + if first:
3.390 + free.append(self.period_class(None, first))
3.391 +
3.392 + start = fb[0].get_end_point()
3.393 +
3.394 + for period in fb[1:]:
3.395 + free.append(self.period_class(start, period.get_start_point()))
3.396 + start = period.get_end_point()
3.397 +
3.398 + # Add an end-of-time period if appropriate.
3.399 +
3.400 + if start:
3.401 + free.append(self.period_class(start, None))
3.402 +
3.403 + return FreeBusyCollection(free)
3.404 +
3.405 + def _update_freebusy(self, periods, uid, recurrenceid):
3.406 +
3.407 + """
3.408 + Update the free/busy details with the given 'periods', using the given
3.409 + 'uid' plus 'recurrenceid' to remove existing periods.
3.410 + """
3.411 +
3.412 + self._check_mutable()
3.413 +
3.414 + self.remove_specific_event_periods(uid, recurrenceid)
3.415 +
3.416 + for p in periods:
3.417 + self.insert_period(p)
3.418 +
3.419 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser):
3.420 +
3.421 + """
3.422 + Update the free/busy details with the given 'periods', 'transp' setting,
3.423 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
3.424 + """
3.425 +
3.426 + new_periods = []
3.427 +
3.428 + for p in periods:
3.429 + new_periods.append(
3.430 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser)
3.431 + )
3.432 +
3.433 + self._update_freebusy(new_periods, uid, recurrenceid)
3.434 +
3.435 +class SupportAttendee:
3.436 +
3.437 + "A mix-in that supports the affected attendee in free/busy periods."
3.438 +
3.439 + period_columns = FreeBusyCollectionBase.period_columns + ["attendee"]
3.440 + period_class = FreeBusyGroupPeriod
3.441 +
3.442 + def _update_freebusy(self, periods, uid, recurrenceid, attendee=None):
3.443 +
3.444 + """
3.445 + Update the free/busy details with the given 'periods', using the given
3.446 + 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods.
3.447 + """
3.448 +
3.449 + self._check_mutable()
3.450 +
3.451 + self.remove_specific_event_periods(uid, recurrenceid, attendee)
3.452 +
3.453 + for p in periods:
3.454 + self.insert_period(p)
3.455 +
3.456 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None):
3.457 +
3.458 + """
3.459 + Update the free/busy details with the given 'periods', 'transp' setting,
3.460 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
3.461 +
3.462 + An optional 'attendee' indicates the attendee affected by the period.
3.463 + """
3.464 +
3.465 + new_periods = []
3.466 +
3.467 + for p in periods:
3.468 + new_periods.append(
3.469 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee)
3.470 + )
3.471 +
3.472 + self._update_freebusy(new_periods, uid, recurrenceid, attendee)
3.473 +
3.474 +class SupportExpires:
3.475 +
3.476 + "A mix-in that supports the expiry datetime in free/busy periods."
3.477 +
3.478 + period_columns = FreeBusyCollectionBase.period_columns + ["expires"]
3.479 + period_class = FreeBusyOfferPeriod
3.480 +
3.481 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
3.482 +
3.483 + """
3.484 + Update the free/busy details with the given 'periods', 'transp' setting,
3.485 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
3.486 +
3.487 + An optional 'expires' datetime string indicates the expiry time of any
3.488 + free/busy offer.
3.489 + """
3.490 +
3.491 + new_periods = []
3.492 +
3.493 + for p in periods:
3.494 + new_periods.append(
3.495 + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)
3.496 + )
3.497 +
3.498 + self._update_freebusy(new_periods, uid, recurrenceid)
3.499 +
3.500 +class FreeBusyCollection(FreeBusyCollectionBase):
3.501 +
3.502 + "An abstraction for a collection of free/busy periods."
3.503 +
3.504 + def __init__(self, periods=None, mutable=True):
3.505 +
3.506 + """
3.507 + Initialise the collection with the given list of 'periods', or start an
3.508 + empty collection if no list is given. If 'mutable' is indicated, the
3.509 + collection may be changed; otherwise, an exception will be raised.
3.510 + """
3.511 +
3.512 + FreeBusyCollectionBase.__init__(self, mutable)
3.513 + self.periods = periods or []
3.514 +
3.515 + # List emulation methods.
3.516 +
3.517 + def __nonzero__(self):
3.518 + return bool(self.periods)
3.519 +
3.520 + def __iter__(self):
3.521 + return iter(self.periods)
3.522 +
3.523 + def __len__(self):
3.524 + return len(self.periods)
3.525 +
3.526 + def __getitem__(self, i):
3.527 + return self.periods[i]
3.528 +
3.529 + # Operations.
3.530 +
3.531 + def insert_period(self, period):
3.532 +
3.533 + "Insert the given 'period' into the collection."
3.534 +
3.535 + self._check_mutable()
3.536 +
3.537 + i = bisect_left(self.periods, period)
3.538 + if i == len(self.periods):
3.539 + self.periods.append(period)
3.540 + elif self.periods[i] != period:
3.541 + self.periods.insert(i, period)
3.542 +
3.543 + def remove_periods(self, periods):
3.544 +
3.545 + "Remove the given 'periods' from the collection."
3.546 +
3.547 + self._check_mutable()
3.548 +
3.549 + for period in periods:
3.550 + i = bisect_left(self.periods, period)
3.551 + if i < len(self.periods) and self.periods[i] == period:
3.552 + del self.periods[i]
3.553 +
3.554 + def remove_event_periods(self, uid, recurrenceid=None):
3.555 +
3.556 + """
3.557 + Remove from the collection all periods associated with 'uid' and
3.558 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
3.559 + be referenced).
3.560 +
3.561 + Return the removed periods.
3.562 + """
3.563 +
3.564 + self._check_mutable()
3.565 +
3.566 + removed = []
3.567 + i = 0
3.568 + while i < len(self.periods):
3.569 + fb = self.periods[i]
3.570 + if fb.uid == uid and fb.recurrenceid == recurrenceid:
3.571 + removed.append(self.periods[i])
3.572 + del self.periods[i]
3.573 + else:
3.574 + i += 1
3.575 +
3.576 + return removed
3.577 +
3.578 + # Specific period removal when updating event details.
3.579 +
3.580 + remove_specific_event_periods = remove_event_periods
3.581 +
3.582 + def remove_additional_periods(self, uid, recurrenceids=None):
3.583 +
3.584 + """
3.585 + Remove from the collection all periods associated with 'uid' having a
3.586 + recurrence identifier indicating an additional or modified period.
3.587 +
3.588 + If 'recurrenceids' is specified, remove all periods associated with
3.589 + 'uid' that do not have a recurrence identifier in the given list.
3.590 +
3.591 + Return the removed periods.
3.592 + """
3.593 +
3.594 + self._check_mutable()
3.595 +
3.596 + removed = []
3.597 + i = 0
3.598 + while i < len(self.periods):
3.599 + fb = self.periods[i]
3.600 + if fb.uid == uid and fb.recurrenceid and (
3.601 + recurrenceids is None or
3.602 + recurrenceids is not None and fb.recurrenceid not in recurrenceids
3.603 + ):
3.604 + removed.append(self.periods[i])
3.605 + del self.periods[i]
3.606 + else:
3.607 + i += 1
3.608 +
3.609 + return removed
3.610 +
3.611 + def remove_affected_period(self, uid, start):
3.612 +
3.613 + """
3.614 + Remove from the collection the period associated with 'uid' that
3.615 + provides an occurrence starting at the given 'start' (provided by a
3.616 + recurrence identifier, converted to a datetime). A recurrence identifier
3.617 + is used to provide an alternative time period whilst also acting as a
3.618 + reference to the originally-defined occurrence.
3.619 +
3.620 + Return any removed period in a list.
3.621 + """
3.622 +
3.623 + self._check_mutable()
3.624 +
3.625 + removed = []
3.626 +
3.627 + search = Period(start, start)
3.628 + found = bisect_left(self.periods, search)
3.629 +
3.630 + while found < len(self.periods):
3.631 + fb = self.periods[found]
3.632 +
3.633 + # Stop looking if the start no longer matches the recurrence identifier.
3.634 +
3.635 + if fb.get_start_point() != search.get_start_point():
3.636 + break
3.637 +
3.638 + # If the period belongs to the parent object, remove it and return.
3.639 +
3.640 + if not fb.recurrenceid and uid == fb.uid:
3.641 + removed.append(self.periods[found])
3.642 + del self.periods[found]
3.643 + break
3.644 +
3.645 + # Otherwise, keep looking for a matching period.
3.646 +
3.647 + found += 1
3.648 +
3.649 + return removed
3.650 +
3.651 + def periods_from(self, period):
3.652 +
3.653 + "Return the entries in the collection at or after 'period'."
3.654 +
3.655 + first = bisect_left(self.periods, period)
3.656 + return self.periods[first:]
3.657 +
3.658 + def periods_until(self, period):
3.659 +
3.660 + "Return the entries in the collection before 'period'."
3.661 +
3.662 + last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
3.663 + return self.periods[:last]
3.664 +
3.665 + def get_overlapping(self, periods):
3.666 +
3.667 + """
3.668 + Return the entries in the collection providing periods overlapping with
3.669 + the given sorted collection of 'periods'.
3.670 + """
3.671 +
3.672 + return get_overlapping(self.periods, periods)
3.673 +
3.674 + def remove_overlapping(self, period):
3.675 +
3.676 + "Remove all periods overlapping with 'period' from the collection."
3.677 +
3.678 + self._check_mutable()
3.679 +
3.680 + overlapping = self.get_overlapping([period])
3.681 +
3.682 + if overlapping:
3.683 + for fb in overlapping:
3.684 + self.periods.remove(fb)
3.685 +
3.686 +class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection):
3.687 +
3.688 + "A collection of quota group free/busy objects."
3.689 +
3.690 + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
3.691 +
3.692 + """
3.693 + Remove from the collection all periods associated with 'uid' and
3.694 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
3.695 + be referenced) and any 'attendee'.
3.696 +
3.697 + Return the removed periods.
3.698 + """
3.699 +
3.700 + self._check_mutable()
3.701 +
3.702 + removed = []
3.703 + i = 0
3.704 + while i < len(self.periods):
3.705 + fb = self.periods[i]
3.706 + if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee:
3.707 + removed.append(self.periods[i])
3.708 + del self.periods[i]
3.709 + else:
3.710 + i += 1
3.711 +
3.712 + return removed
3.713 +
3.714 +class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection):
3.715 +
3.716 + "A collection of offered free/busy objects."
3.717 +
3.718 + pass
3.719 +
3.720 +class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations):
3.721 +
3.722 + """
3.723 + An abstraction for a collection of free/busy periods stored in a database
3.724 + system.
3.725 + """
3.726 +
3.727 + def __init__(self, cursor, table_name, column_names=None, filter_values=None,
3.728 + mutable=True, paramstyle=None):
3.729 +
3.730 + """
3.731 + Initialise the collection with the given 'cursor' and with the
3.732 + 'table_name', 'column_names' and 'filter_values' configuring the
3.733 + selection of data. If 'mutable' is indicated, the collection may be
3.734 + changed; otherwise, an exception will be raised.
3.735 + """
3.736 +
3.737 + FreeBusyCollectionBase.__init__(self, mutable)
3.738 + DatabaseOperations.__init__(self, column_names, filter_values, paramstyle)
3.739 + self.cursor = cursor
3.740 + self.table_name = table_name
3.741 +
3.742 + # List emulation methods.
3.743 +
3.744 + def __nonzero__(self):
3.745 + return len(self) and True or False
3.746 +
3.747 + def __iter__(self):
3.748 + query, values = self.get_query(
3.749 + "select %(columns)s from %(table)s :condition" % {
3.750 + "columns" : self.columnlist(self.period_columns),
3.751 + "table" : self.table_name
3.752 + })
3.753 + self.cursor.execute(query, values)
3.754 + return iter(map(lambda t: self.make_period(t), self.cursor.fetchall()))
3.755 +
3.756 + def __len__(self):
3.757 + query, values = self.get_query(
3.758 + "select count(*) from %(table)s :condition" % {
3.759 + "table" : self.table_name
3.760 + })
3.761 + self.cursor.execute(query, values)
3.762 + result = self.cursor.fetchone()
3.763 + return result and int(result[0]) or 0
3.764 +
3.765 + def __getitem__(self, i):
3.766 + return list(iter(self))[i]
3.767 +
3.768 + # Operations.
3.769 +
3.770 + def insert_period(self, period):
3.771 +
3.772 + "Insert the given 'period' into the collection."
3.773 +
3.774 + self._check_mutable()
3.775 +
3.776 + columns, values = self.period_columns, period.as_tuple(string_datetimes=True)
3.777 +
3.778 + query, values = self.get_query(
3.779 + "insert into %(table)s (:columns) values (:values)" % {
3.780 + "table" : self.table_name
3.781 + },
3.782 + columns, [to_string(v, "utf-8") for v in values])
3.783 +
3.784 + self.cursor.execute(query, values)
3.785 +
3.786 + def remove_periods(self, periods):
3.787 +
3.788 + "Remove the given 'periods' from the collection."
3.789 +
3.790 + self._check_mutable()
3.791 +
3.792 + for period in periods:
3.793 + values = period.as_tuple(string_datetimes=True)
3.794 +
3.795 + query, values = self.get_query(
3.796 + "delete from %(table)s :condition" % {
3.797 + "table" : self.table_name
3.798 + },
3.799 + self.period_columns, [to_string(v, "utf-8") for v in values])
3.800 +
3.801 + self.cursor.execute(query, values)
3.802 +
3.803 + def remove_event_periods(self, uid, recurrenceid=None):
3.804 +
3.805 + """
3.806 + Remove from the collection all periods associated with 'uid' and
3.807 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
3.808 + be referenced).
3.809 +
3.810 + Return the removed periods.
3.811 + """
3.812 +
3.813 + self._check_mutable()
3.814 +
3.815 + if recurrenceid:
3.816 + columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid]
3.817 + else:
3.818 + columns, values = ["object_uid", "object_recurrenceid is null"], [uid]
3.819 +
3.820 + query, _values = self.get_query(
3.821 + "select %(columns)s from %(table)s :condition" % {
3.822 + "columns" : self.columnlist(self.period_columns),
3.823 + "table" : self.table_name
3.824 + },
3.825 + columns, values)
3.826 +
3.827 + self.cursor.execute(query, _values)
3.828 + removed = self.cursor.fetchall()
3.829 +
3.830 + query, values = self.get_query(
3.831 + "delete from %(table)s :condition" % {
3.832 + "table" : self.table_name
3.833 + },
3.834 + columns, values)
3.835 +
3.836 + self.cursor.execute(query, values)
3.837 +
3.838 + return map(lambda t: self.make_period(t), removed)
3.839 +
3.840 + # Specific period removal when updating event details.
3.841 +
3.842 + remove_specific_event_periods = remove_event_periods
3.843 +
3.844 + def remove_additional_periods(self, uid, recurrenceids=None):
3.845 +
3.846 + """
3.847 + Remove from the collection all periods associated with 'uid' having a
3.848 + recurrence identifier indicating an additional or modified period.
3.849 +
3.850 + If 'recurrenceids' is specified, remove all periods associated with
3.851 + 'uid' that do not have a recurrence identifier in the given list.
3.852 +
3.853 + Return the removed periods.
3.854 + """
3.855 +
3.856 + self._check_mutable()
3.857 +
3.858 + if not recurrenceids:
3.859 + columns, values = ["object_uid", "object_recurrenceid is not null"], [uid]
3.860 + else:
3.861 + columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)]
3.862 +
3.863 + query, _values = self.get_query(
3.864 + "select %(columns)s from %(table)s :condition" % {
3.865 + "columns" : self.columnlist(self.period_columns),
3.866 + "table" : self.table_name
3.867 + },
3.868 + columns, values)
3.869 +
3.870 + self.cursor.execute(query, _values)
3.871 + removed = self.cursor.fetchall()
3.872 +
3.873 + query, values = self.get_query(
3.874 + "delete from %(table)s :condition" % {
3.875 + "table" : self.table_name
3.876 + },
3.877 + columns, values)
3.878 +
3.879 + self.cursor.execute(query, values)
3.880 +
3.881 + return map(lambda t: self.make_period(t), removed)
3.882 +
3.883 + def remove_affected_period(self, uid, start):
3.884 +
3.885 + """
3.886 + Remove from the collection the period associated with 'uid' that
3.887 + provides an occurrence starting at the given 'start' (provided by a
3.888 + recurrence identifier, converted to a datetime). A recurrence identifier
3.889 + is used to provide an alternative time period whilst also acting as a
3.890 + reference to the originally-defined occurrence.
3.891 +
3.892 + Return any removed period in a list.
3.893 + """
3.894 +
3.895 + self._check_mutable()
3.896 +
3.897 + start = format_datetime(start)
3.898 +
3.899 + columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start]
3.900 +
3.901 + query, _values = self.get_query(
3.902 + "select %(columns)s from %(table)s :condition" % {
3.903 + "columns" : self.columnlist(self.period_columns),
3.904 + "table" : self.table_name
3.905 + },
3.906 + columns, values)
3.907 +
3.908 + self.cursor.execute(query, _values)
3.909 + removed = self.cursor.fetchall()
3.910 +
3.911 + query, values = self.get_query(
3.912 + "delete from %(table)s :condition" % {
3.913 + "table" : self.table_name
3.914 + },
3.915 + columns, values)
3.916 +
3.917 + self.cursor.execute(query, values)
3.918 +
3.919 + return map(lambda t: self.make_period(t), removed)
3.920 +
3.921 + def periods_from(self, period):
3.922 +
3.923 + "Return the entries in the collection at or after 'period'."
3.924 +
3.925 + start = format_datetime(period.get_start_point())
3.926 +
3.927 + columns, values = [], []
3.928 +
3.929 + if start:
3.930 + columns.append("start >= ?")
3.931 + values.append(start)
3.932 +
3.933 + query, values = self.get_query(
3.934 + "select %(columns)s from %(table)s :condition" % {
3.935 + "columns" : self.columnlist(self.period_columns),
3.936 + "table" : self.table_name
3.937 + },
3.938 + columns, values)
3.939 +
3.940 + self.cursor.execute(query, values)
3.941 +
3.942 + return map(lambda t: self.make_period(t), self.cursor.fetchall())
3.943 +
3.944 + def periods_until(self, period):
3.945 +
3.946 + "Return the entries in the collection before 'period'."
3.947 +
3.948 + end = format_datetime(period.get_end_point())
3.949 +
3.950 + columns, values = [], []
3.951 +
3.952 + if end:
3.953 + columns.append("start < ?")
3.954 + values.append(end)
3.955 +
3.956 + query, values = self.get_query(
3.957 + "select %(columns)s from %(table)s :condition" % {
3.958 + "columns" : self.columnlist(self.period_columns),
3.959 + "table" : self.table_name
3.960 + },
3.961 + columns, values)
3.962 +
3.963 + self.cursor.execute(query, values)
3.964 +
3.965 + return map(lambda t: self.make_period(t), self.cursor.fetchall())
3.966 +
3.967 + def get_overlapping(self, periods):
3.968 +
3.969 + """
3.970 + Return the entries in the collection providing periods overlapping with
3.971 + the given sorted collection of 'periods'.
3.972 + """
3.973 +
3.974 + overlapping = set()
3.975 +
3.976 + for period in periods:
3.977 + columns, values = self._get_period_values(period)
3.978 +
3.979 + query, values = self.get_query(
3.980 + "select %(columns)s from %(table)s :condition" % {
3.981 + "columns" : self.columnlist(self.period_columns),
3.982 + "table" : self.table_name
3.983 + },
3.984 + columns, values)
3.985 +
3.986 + self.cursor.execute(query, values)
3.987 +
3.988 + overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall()))
3.989 +
3.990 + overlapping = list(overlapping)
3.991 + overlapping.sort()
3.992 + return overlapping
3.993 +
3.994 + def remove_overlapping(self, period):
3.995 +
3.996 + "Remove all periods overlapping with 'period' from the collection."
3.997 +
3.998 + self._check_mutable()
3.999 +
3.1000 + columns, values = self._get_period_values(period)
3.1001 +
3.1002 + query, values = self.get_query(
3.1003 + "delete from %(table)s :condition" % {
3.1004 + "table" : self.table_name
3.1005 + },
3.1006 + columns, values)
3.1007 +
3.1008 + self.cursor.execute(query, values)
3.1009 +
3.1010 + def _get_period_values(self, period):
3.1011 +
3.1012 + start = format_datetime(period.get_start_point())
3.1013 + end = format_datetime(period.get_end_point())
3.1014 +
3.1015 + columns, values = [], []
3.1016 +
3.1017 + if end:
3.1018 + columns.append("start < ?")
3.1019 + values.append(end)
3.1020 + if start:
3.1021 + columns.append("end > ?")
3.1022 + values.append(start)
3.1023 +
3.1024 + return columns, values
3.1025 +
3.1026 +class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection):
3.1027 +
3.1028 + "A collection of quota group free/busy objects."
3.1029 +
3.1030 + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
3.1031 +
3.1032 + """
3.1033 + Remove from the collection all periods associated with 'uid' and
3.1034 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
3.1035 + be referenced) and any 'attendee'.
3.1036 +
3.1037 + Return the removed periods.
3.1038 + """
3.1039 +
3.1040 + self._check_mutable()
3.1041 +
3.1042 + columns, values = ["object_uid"], [uid]
3.1043 +
3.1044 + if recurrenceid:
3.1045 + columns.append("object_recurrenceid")
3.1046 + values.append(recurrenceid)
3.1047 + else:
3.1048 + columns.append("object_recurrenceid is null")
3.1049 +
3.1050 + if attendee:
3.1051 + columns.append("attendee")
3.1052 + values.append(attendee)
3.1053 + else:
3.1054 + columns.append("attendee is null")
3.1055 +
3.1056 + query, _values = self.get_query(
3.1057 + "select %(columns)s from %(table)s :condition" % {
3.1058 + "columns" : self.columnlist(self.period_columns),
3.1059 + "table" : self.table_name
3.1060 + },
3.1061 + columns, values)
3.1062 +
3.1063 + self.cursor.execute(query, _values)
3.1064 + removed = self.cursor.fetchall()
3.1065 +
3.1066 + query, values = self.get_query(
3.1067 + "delete from %(table)s :condition" % {
3.1068 + "table" : self.table_name
3.1069 + },
3.1070 + columns, values)
3.1071 +
3.1072 + self.cursor.execute(query, values)
3.1073 +
3.1074 + return map(lambda t: self.make_period(t), removed)
3.1075 +
3.1076 +class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection):
3.1077 +
3.1078 + "A collection of offered free/busy objects."
3.1079 +
3.1080 + pass
3.1081 +
3.1082 +# vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/imiptools/handlers/common.py Tue May 23 16:31:27 2017 +0200
4.2 +++ b/imiptools/handlers/common.py Tue May 23 16:34:09 2017 +0200
4.3 @@ -3,7 +3,7 @@
4.4 """
4.5 Common handler functionality for different entities.
4.6
4.7 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
4.8 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
4.9
4.10 This program is free software; you can redistribute it and/or modify it under
4.11 the terms of the GNU General Public License as published by the Free Software
4.12 @@ -22,7 +22,8 @@
4.13 from imiptools.data import get_address, get_uri, make_freebusy, to_part, \
4.14 uri_dict
4.15 from imiptools.dates import format_datetime
4.16 -from imiptools.period import FreeBusyPeriod, Period
4.17 +from imiptools.freebusy import FreeBusyPeriod
4.18 +from imiptools.period import Period
4.19
4.20 class CommonFreebusy:
4.21
5.1 --- a/imiptools/period.py Tue May 23 16:31:27 2017 +0200
5.2 +++ b/imiptools/period.py Tue May 23 16:34:09 2017 +0200
5.3 @@ -3,7 +3,7 @@
5.4 """
5.5 Managing and presenting periods of time.
5.6
5.7 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
5.8 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
5.9
5.10 This program is free software; you can redistribute it and/or modify it under
5.11 the terms of the GNU General Public License as published by the Free Software
5.12 @@ -19,33 +19,20 @@
5.13 this program. If not, see <http://www.gnu.org/licenses/>.
5.14 """
5.15
5.16 -from bisect import bisect_left, bisect_right, insort_left
5.17 +from bisect import bisect_left, insort_left
5.18 from datetime import date, datetime, timedelta
5.19 from imiptools.dates import check_permitted_values, correct_datetime, \
5.20 - format_datetime, get_datetime, \
5.21 + get_datetime, \
5.22 get_datetime_attributes, \
5.23 get_recurrence_start, get_recurrence_start_point, \
5.24 get_start_of_day, \
5.25 get_tzid, \
5.26 to_timezone, to_utc_datetime
5.27 -from imiptools.sql import DatabaseOperations
5.28
5.29 def ifnone(x, y):
5.30 if x is None: return y
5.31 else: return x
5.32
5.33 -def from_string(s, encoding):
5.34 - if s:
5.35 - return unicode(s, encoding)
5.36 - else:
5.37 - return s
5.38 -
5.39 -def to_string(s, encoding):
5.40 - if s:
5.41 - return s.encode(encoding)
5.42 - else:
5.43 - return s
5.44 -
5.45 class Comparable:
5.46
5.47 "A date/datetime wrapper that allows comparisons with other types."
5.48 @@ -357,184 +344,6 @@
5.49 def make_corrected(self, start, end):
5.50 return self.__class__(start, end, self.tzid, self.origin)
5.51
5.52 -class FreeBusyPeriod(PeriodBase):
5.53 -
5.54 - "A free/busy record abstraction."
5.55 -
5.56 - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
5.57 - summary=None, organiser=None):
5.58 -
5.59 - """
5.60 - Initialise a free/busy period with the given 'start' and 'end' points,
5.61 - plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
5.62 - details.
5.63 - """
5.64 -
5.65 - PeriodBase.__init__(self, start, end)
5.66 - self.uid = uid
5.67 - self.transp = transp or None
5.68 - self.recurrenceid = recurrenceid or None
5.69 - self.summary = summary or None
5.70 - self.organiser = organiser or None
5.71 -
5.72 - def as_tuple(self, strings_only=False, string_datetimes=False):
5.73 -
5.74 - """
5.75 - Return the initialisation parameter tuple, converting datetimes and
5.76 - false value parameters to strings if 'strings_only' is set to a true
5.77 - value. Otherwise, if 'string_datetimes' is set to a true value, only the
5.78 - datetime values are converted to strings.
5.79 - """
5.80 -
5.81 - null = lambda x: (strings_only and [""] or [x])[0]
5.82 - return (
5.83 - (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
5.84 - (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
5.85 - self.uid or null(self.uid),
5.86 - self.transp or strings_only and "OPAQUE" or None,
5.87 - self.recurrenceid or null(self.recurrenceid),
5.88 - self.summary or null(self.summary),
5.89 - self.organiser or null(self.organiser)
5.90 - )
5.91 -
5.92 - def __cmp__(self, other):
5.93 -
5.94 - """
5.95 - Compare this object to 'other', employing the uid if the periods
5.96 - involved are the same.
5.97 - """
5.98 -
5.99 - result = PeriodBase.__cmp__(self, other)
5.100 - if result == 0 and isinstance(other, FreeBusyPeriod):
5.101 - return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))
5.102 - else:
5.103 - return result
5.104 -
5.105 - def get_key(self):
5.106 - return self.uid, self.recurrenceid, self.get_start()
5.107 -
5.108 - def __repr__(self):
5.109 - return "FreeBusyPeriod%r" % (self.as_tuple(),)
5.110 -
5.111 - def get_tzid(self):
5.112 - return "UTC"
5.113 -
5.114 - # Period and event recurrence logic.
5.115 -
5.116 - def is_replaced(self, recurrences):
5.117 -
5.118 - """
5.119 - Return whether this period refers to one of the 'recurrences'.
5.120 - The 'recurrences' must be UTC datetimes corresponding to the start of
5.121 - the period described by a recurrence.
5.122 - """
5.123 -
5.124 - for recurrence in recurrences:
5.125 - if self.is_affected(recurrence):
5.126 - return True
5.127 - return False
5.128 -
5.129 - def is_affected(self, recurrence):
5.130 -
5.131 - """
5.132 - Return whether this period refers to 'recurrence'. The 'recurrence' must
5.133 - be a UTC datetime corresponding to the start of the period described by
5.134 - a recurrence.
5.135 - """
5.136 -
5.137 - return recurrence and self.get_start_point() == recurrence
5.138 -
5.139 - # Value correction methods.
5.140 -
5.141 - def make_corrected(self, start, end):
5.142 - return self.__class__(start, end)
5.143 -
5.144 -class FreeBusyOfferPeriod(FreeBusyPeriod):
5.145 -
5.146 - "A free/busy record abstraction for an offer period."
5.147 -
5.148 - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
5.149 - summary=None, organiser=None, expires=None):
5.150 -
5.151 - """
5.152 - Initialise a free/busy period with the given 'start' and 'end' points,
5.153 - plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
5.154 - details.
5.155 -
5.156 - An additional 'expires' parameter can be used to indicate an expiry
5.157 - datetime in conjunction with free/busy offers made when countering
5.158 - event proposals.
5.159 - """
5.160 -
5.161 - FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
5.162 - summary, organiser)
5.163 - self.expires = expires or None
5.164 -
5.165 - def as_tuple(self, strings_only=False, string_datetimes=False):
5.166 -
5.167 - """
5.168 - Return the initialisation parameter tuple, converting datetimes and
5.169 - false value parameters to strings if 'strings_only' is set to a true
5.170 - value. Otherwise, if 'string_datetimes' is set to a true value, only the
5.171 - datetime values are converted to strings.
5.172 - """
5.173 -
5.174 - null = lambda x: (strings_only and [""] or [x])[0]
5.175 - return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
5.176 - self.expires or null(self.expires),)
5.177 -
5.178 - def __repr__(self):
5.179 - return "FreeBusyOfferPeriod%r" % (self.as_tuple(),)
5.180 -
5.181 -class FreeBusyGroupPeriod(FreeBusyPeriod):
5.182 -
5.183 - "A free/busy record abstraction for a quota group period."
5.184 -
5.185 - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
5.186 - summary=None, organiser=None, attendee=None):
5.187 -
5.188 - """
5.189 - Initialise a free/busy period with the given 'start' and 'end' points,
5.190 - plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
5.191 - details.
5.192 -
5.193 - An additional 'attendee' parameter can be used to indicate the identity
5.194 - of the attendee recording the period.
5.195 - """
5.196 -
5.197 - FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
5.198 - summary, organiser)
5.199 - self.attendee = attendee or None
5.200 -
5.201 - def as_tuple(self, strings_only=False, string_datetimes=False):
5.202 -
5.203 - """
5.204 - Return the initialisation parameter tuple, converting datetimes and
5.205 - false value parameters to strings if 'strings_only' is set to a true
5.206 - value. Otherwise, if 'string_datetimes' is set to a true value, only the
5.207 - datetime values are converted to strings.
5.208 - """
5.209 -
5.210 - null = lambda x: (strings_only and [""] or [x])[0]
5.211 - return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
5.212 - self.attendee or null(self.attendee),)
5.213 -
5.214 - def __cmp__(self, other):
5.215 -
5.216 - """
5.217 - Compare this object to 'other', employing the uid if the periods
5.218 - involved are the same.
5.219 - """
5.220 -
5.221 - result = FreeBusyPeriod.__cmp__(self, other)
5.222 - if isinstance(other, FreeBusyGroupPeriod) and result == 0:
5.223 - return cmp(self.attendee, other.attendee)
5.224 - else:
5.225 - return result
5.226 -
5.227 - def __repr__(self):
5.228 - return "FreeBusyGroupPeriod%r" % (self.as_tuple(),)
5.229 -
5.230 class RecurringPeriod(Period):
5.231
5.232 """
5.233 @@ -562,867 +371,6 @@
5.234 def make_corrected(self, start, end):
5.235 return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
5.236
5.237 -class FreeBusyCollectionBase:
5.238 -
5.239 - "Common operations on free/busy period collections."
5.240 -
5.241 - period_columns = [
5.242 - "start", "end", "object_uid", "transp", "object_recurrenceid",
5.243 - "summary", "organiser"
5.244 - ]
5.245 -
5.246 - period_class = FreeBusyPeriod
5.247 -
5.248 - def __init__(self, mutable=True):
5.249 - self.mutable = mutable
5.250 -
5.251 - def _check_mutable(self):
5.252 - if not self.mutable:
5.253 - raise TypeError, "Cannot mutate this collection."
5.254 -
5.255 - def copy(self):
5.256 -
5.257 - "Make an independent mutable copy of the collection."
5.258 -
5.259 - return FreeBusyCollection(list(self), True)
5.260 -
5.261 - def make_period(self, t):
5.262 -
5.263 - """
5.264 - Make a period using the given tuple of arguments and the collection's
5.265 - column details.
5.266 - """
5.267 -
5.268 - args = []
5.269 - for arg, column in zip(t, self.period_columns):
5.270 - args.append(from_string(arg, "utf-8"))
5.271 - return self.period_class(*args)
5.272 -
5.273 - def make_tuple(self, t):
5.274 -
5.275 - """
5.276 - Return a tuple from the given tuple 't' conforming to the collection's
5.277 - column details.
5.278 - """
5.279 -
5.280 - args = []
5.281 - for arg, column in zip(t, self.period_columns):
5.282 - args.append(arg)
5.283 - return tuple(args)
5.284 -
5.285 - # List emulation methods.
5.286 -
5.287 - def __iadd__(self, periods):
5.288 - for period in periods:
5.289 - self.insert_period(period)
5.290 - return self
5.291 -
5.292 - def append(self, period):
5.293 - self.insert_period(period)
5.294 -
5.295 - # Operations.
5.296 -
5.297 - def can_schedule(self, periods, uid, recurrenceid):
5.298 -
5.299 - """
5.300 - Return whether the collection can accommodate the given 'periods'
5.301 - employing the specified 'uid' and 'recurrenceid'.
5.302 - """
5.303 -
5.304 - for conflict in self.have_conflict(periods, True):
5.305 - if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
5.306 - return False
5.307 -
5.308 - return True
5.309 -
5.310 - def have_conflict(self, periods, get_conflicts=False):
5.311 -
5.312 - """
5.313 - Return whether any period in the collection overlaps with the given
5.314 - 'periods', returning a collection of such overlapping periods if
5.315 - 'get_conflicts' is set to a true value.
5.316 - """
5.317 -
5.318 - conflicts = set()
5.319 - for p in periods:
5.320 - overlapping = self.period_overlaps(p, get_conflicts)
5.321 - if overlapping:
5.322 - if get_conflicts:
5.323 - conflicts.update(overlapping)
5.324 - else:
5.325 - return True
5.326 -
5.327 - if get_conflicts:
5.328 - return conflicts
5.329 - else:
5.330 - return False
5.331 -
5.332 - def period_overlaps(self, period, get_periods=False):
5.333 -
5.334 - """
5.335 - Return whether any period in the collection overlaps with the given
5.336 - 'period', returning a collection of overlapping periods if 'get_periods'
5.337 - is set to a true value.
5.338 - """
5.339 -
5.340 - overlapping = self.get_overlapping([period])
5.341 -
5.342 - if get_periods:
5.343 - return overlapping
5.344 - else:
5.345 - return len(overlapping) != 0
5.346 -
5.347 - def replace_overlapping(self, period, replacements):
5.348 -
5.349 - """
5.350 - Replace existing periods in the collection within the given 'period',
5.351 - using the given 'replacements'.
5.352 - """
5.353 -
5.354 - self._check_mutable()
5.355 -
5.356 - self.remove_overlapping(period)
5.357 - for replacement in replacements:
5.358 - self.insert_period(replacement)
5.359 -
5.360 - def coalesce_freebusy(self):
5.361 -
5.362 - "Coalesce the periods in the collection, returning a new collection."
5.363 -
5.364 - if not self:
5.365 - return FreeBusyCollection()
5.366 -
5.367 - fb = []
5.368 -
5.369 - it = iter(self)
5.370 - period = it.next()
5.371 -
5.372 - start = period.get_start_point()
5.373 - end = period.get_end_point()
5.374 -
5.375 - try:
5.376 - while True:
5.377 - period = it.next()
5.378 - if period.get_start_point() > end:
5.379 - fb.append(self.period_class(start, end))
5.380 - start = period.get_start_point()
5.381 - end = period.get_end_point()
5.382 - else:
5.383 - end = max(end, period.get_end_point())
5.384 - except StopIteration:
5.385 - pass
5.386 -
5.387 - fb.append(self.period_class(start, end))
5.388 - return FreeBusyCollection(fb)
5.389 -
5.390 - def invert_freebusy(self):
5.391 -
5.392 - "Return the free periods from the collection as a new collection."
5.393 -
5.394 - if not self:
5.395 - return FreeBusyCollection([self.period_class(None, None)])
5.396 -
5.397 - # Coalesce periods that overlap or are adjacent.
5.398 -
5.399 - fb = self.coalesce_freebusy()
5.400 - free = []
5.401 -
5.402 - # Add a start-of-time period if appropriate.
5.403 -
5.404 - first = fb[0].get_start_point()
5.405 - if first:
5.406 - free.append(self.period_class(None, first))
5.407 -
5.408 - start = fb[0].get_end_point()
5.409 -
5.410 - for period in fb[1:]:
5.411 - free.append(self.period_class(start, period.get_start_point()))
5.412 - start = period.get_end_point()
5.413 -
5.414 - # Add an end-of-time period if appropriate.
5.415 -
5.416 - if start:
5.417 - free.append(self.period_class(start, None))
5.418 -
5.419 - return FreeBusyCollection(free)
5.420 -
5.421 - def _update_freebusy(self, periods, uid, recurrenceid):
5.422 -
5.423 - """
5.424 - Update the free/busy details with the given 'periods', using the given
5.425 - 'uid' plus 'recurrenceid' to remove existing periods.
5.426 - """
5.427 -
5.428 - self._check_mutable()
5.429 -
5.430 - self.remove_specific_event_periods(uid, recurrenceid)
5.431 -
5.432 - for p in periods:
5.433 - self.insert_period(p)
5.434 -
5.435 - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser):
5.436 -
5.437 - """
5.438 - Update the free/busy details with the given 'periods', 'transp' setting,
5.439 - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
5.440 - """
5.441 -
5.442 - new_periods = []
5.443 -
5.444 - for p in periods:
5.445 - new_periods.append(
5.446 - self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser)
5.447 - )
5.448 -
5.449 - self._update_freebusy(new_periods, uid, recurrenceid)
5.450 -
5.451 -class SupportAttendee:
5.452 -
5.453 - "A mix-in that supports the affected attendee in free/busy periods."
5.454 -
5.455 - period_columns = FreeBusyCollectionBase.period_columns + ["attendee"]
5.456 - period_class = FreeBusyGroupPeriod
5.457 -
5.458 - def _update_freebusy(self, periods, uid, recurrenceid, attendee=None):
5.459 -
5.460 - """
5.461 - Update the free/busy details with the given 'periods', using the given
5.462 - 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods.
5.463 - """
5.464 -
5.465 - self._check_mutable()
5.466 -
5.467 - self.remove_specific_event_periods(uid, recurrenceid, attendee)
5.468 -
5.469 - for p in periods:
5.470 - self.insert_period(p)
5.471 -
5.472 - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None):
5.473 -
5.474 - """
5.475 - Update the free/busy details with the given 'periods', 'transp' setting,
5.476 - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
5.477 -
5.478 - An optional 'attendee' indicates the attendee affected by the period.
5.479 - """
5.480 -
5.481 - new_periods = []
5.482 -
5.483 - for p in periods:
5.484 - new_periods.append(
5.485 - self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee)
5.486 - )
5.487 -
5.488 - self._update_freebusy(new_periods, uid, recurrenceid, attendee)
5.489 -
5.490 -class SupportExpires:
5.491 -
5.492 - "A mix-in that supports the expiry datetime in free/busy periods."
5.493 -
5.494 - period_columns = FreeBusyCollectionBase.period_columns + ["expires"]
5.495 - period_class = FreeBusyOfferPeriod
5.496 -
5.497 - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
5.498 -
5.499 - """
5.500 - Update the free/busy details with the given 'periods', 'transp' setting,
5.501 - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
5.502 -
5.503 - An optional 'expires' datetime string indicates the expiry time of any
5.504 - free/busy offer.
5.505 - """
5.506 -
5.507 - new_periods = []
5.508 -
5.509 - for p in periods:
5.510 - new_periods.append(
5.511 - self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)
5.512 - )
5.513 -
5.514 - self._update_freebusy(new_periods, uid, recurrenceid)
5.515 -
5.516 -class FreeBusyCollection(FreeBusyCollectionBase):
5.517 -
5.518 - "An abstraction for a collection of free/busy periods."
5.519 -
5.520 - def __init__(self, periods=None, mutable=True):
5.521 -
5.522 - """
5.523 - Initialise the collection with the given list of 'periods', or start an
5.524 - empty collection if no list is given. If 'mutable' is indicated, the
5.525 - collection may be changed; otherwise, an exception will be raised.
5.526 - """
5.527 -
5.528 - FreeBusyCollectionBase.__init__(self, mutable)
5.529 - self.periods = periods or []
5.530 -
5.531 - # List emulation methods.
5.532 -
5.533 - def __nonzero__(self):
5.534 - return bool(self.periods)
5.535 -
5.536 - def __iter__(self):
5.537 - return iter(self.periods)
5.538 -
5.539 - def __len__(self):
5.540 - return len(self.periods)
5.541 -
5.542 - def __getitem__(self, i):
5.543 - return self.periods[i]
5.544 -
5.545 - # Operations.
5.546 -
5.547 - def insert_period(self, period):
5.548 -
5.549 - "Insert the given 'period' into the collection."
5.550 -
5.551 - self._check_mutable()
5.552 -
5.553 - i = bisect_left(self.periods, period)
5.554 - if i == len(self.periods):
5.555 - self.periods.append(period)
5.556 - elif self.periods[i] != period:
5.557 - self.periods.insert(i, period)
5.558 -
5.559 - def remove_periods(self, periods):
5.560 -
5.561 - "Remove the given 'periods' from the collection."
5.562 -
5.563 - self._check_mutable()
5.564 -
5.565 - for period in periods:
5.566 - i = bisect_left(self.periods, period)
5.567 - if i < len(self.periods) and self.periods[i] == period:
5.568 - del self.periods[i]
5.569 -
5.570 - def remove_event_periods(self, uid, recurrenceid=None):
5.571 -
5.572 - """
5.573 - Remove from the collection all periods associated with 'uid' and
5.574 - 'recurrenceid' (which if omitted causes the "parent" object's periods to
5.575 - be referenced).
5.576 -
5.577 - Return the removed periods.
5.578 - """
5.579 -
5.580 - self._check_mutable()
5.581 -
5.582 - removed = []
5.583 - i = 0
5.584 - while i < len(self.periods):
5.585 - fb = self.periods[i]
5.586 - if fb.uid == uid and fb.recurrenceid == recurrenceid:
5.587 - removed.append(self.periods[i])
5.588 - del self.periods[i]
5.589 - else:
5.590 - i += 1
5.591 -
5.592 - return removed
5.593 -
5.594 - # Specific period removal when updating event details.
5.595 -
5.596 - remove_specific_event_periods = remove_event_periods
5.597 -
5.598 - def remove_additional_periods(self, uid, recurrenceids=None):
5.599 -
5.600 - """
5.601 - Remove from the collection all periods associated with 'uid' having a
5.602 - recurrence identifier indicating an additional or modified period.
5.603 -
5.604 - If 'recurrenceids' is specified, remove all periods associated with
5.605 - 'uid' that do not have a recurrence identifier in the given list.
5.606 -
5.607 - Return the removed periods.
5.608 - """
5.609 -
5.610 - self._check_mutable()
5.611 -
5.612 - removed = []
5.613 - i = 0
5.614 - while i < len(self.periods):
5.615 - fb = self.periods[i]
5.616 - if fb.uid == uid and fb.recurrenceid and (
5.617 - recurrenceids is None or
5.618 - recurrenceids is not None and fb.recurrenceid not in recurrenceids
5.619 - ):
5.620 - removed.append(self.periods[i])
5.621 - del self.periods[i]
5.622 - else:
5.623 - i += 1
5.624 -
5.625 - return removed
5.626 -
5.627 - def remove_affected_period(self, uid, start):
5.628 -
5.629 - """
5.630 - Remove from the collection the period associated with 'uid' that
5.631 - provides an occurrence starting at the given 'start' (provided by a
5.632 - recurrence identifier, converted to a datetime). A recurrence identifier
5.633 - is used to provide an alternative time period whilst also acting as a
5.634 - reference to the originally-defined occurrence.
5.635 -
5.636 - Return any removed period in a list.
5.637 - """
5.638 -
5.639 - self._check_mutable()
5.640 -
5.641 - removed = []
5.642 -
5.643 - search = Period(start, start)
5.644 - found = bisect_left(self.periods, search)
5.645 -
5.646 - while found < len(self.periods):
5.647 - fb = self.periods[found]
5.648 -
5.649 - # Stop looking if the start no longer matches the recurrence identifier.
5.650 -
5.651 - if fb.get_start_point() != search.get_start_point():
5.652 - break
5.653 -
5.654 - # If the period belongs to the parent object, remove it and return.
5.655 -
5.656 - if not fb.recurrenceid and uid == fb.uid:
5.657 - removed.append(self.periods[found])
5.658 - del self.periods[found]
5.659 - break
5.660 -
5.661 - # Otherwise, keep looking for a matching period.
5.662 -
5.663 - found += 1
5.664 -
5.665 - return removed
5.666 -
5.667 - def periods_from(self, period):
5.668 -
5.669 - "Return the entries in the collection at or after 'period'."
5.670 -
5.671 - first = bisect_left(self.periods, period)
5.672 - return self.periods[first:]
5.673 -
5.674 - def periods_until(self, period):
5.675 -
5.676 - "Return the entries in the collection before 'period'."
5.677 -
5.678 - last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
5.679 - return self.periods[:last]
5.680 -
5.681 - def get_overlapping(self, periods):
5.682 -
5.683 - """
5.684 - Return the entries in the collection providing periods overlapping with
5.685 - the given sorted collection of 'periods'.
5.686 - """
5.687 -
5.688 - return get_overlapping(self.periods, periods)
5.689 -
5.690 - def remove_overlapping(self, period):
5.691 -
5.692 - "Remove all periods overlapping with 'period' from the collection."
5.693 -
5.694 - self._check_mutable()
5.695 -
5.696 - overlapping = self.get_overlapping([period])
5.697 -
5.698 - if overlapping:
5.699 - for fb in overlapping:
5.700 - self.periods.remove(fb)
5.701 -
5.702 -class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection):
5.703 -
5.704 - "A collection of quota group free/busy objects."
5.705 -
5.706 - def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
5.707 -
5.708 - """
5.709 - Remove from the collection all periods associated with 'uid' and
5.710 - 'recurrenceid' (which if omitted causes the "parent" object's periods to
5.711 - be referenced) and any 'attendee'.
5.712 -
5.713 - Return the removed periods.
5.714 - """
5.715 -
5.716 - self._check_mutable()
5.717 -
5.718 - removed = []
5.719 - i = 0
5.720 - while i < len(self.periods):
5.721 - fb = self.periods[i]
5.722 - if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee:
5.723 - removed.append(self.periods[i])
5.724 - del self.periods[i]
5.725 - else:
5.726 - i += 1
5.727 -
5.728 - return removed
5.729 -
5.730 -class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection):
5.731 -
5.732 - "A collection of offered free/busy objects."
5.733 -
5.734 - pass
5.735 -
5.736 -class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations):
5.737 -
5.738 - """
5.739 - An abstraction for a collection of free/busy periods stored in a database
5.740 - system.
5.741 - """
5.742 -
5.743 - def __init__(self, cursor, table_name, column_names=None, filter_values=None,
5.744 - mutable=True, paramstyle=None):
5.745 -
5.746 - """
5.747 - Initialise the collection with the given 'cursor' and with the
5.748 - 'table_name', 'column_names' and 'filter_values' configuring the
5.749 - selection of data. If 'mutable' is indicated, the collection may be
5.750 - changed; otherwise, an exception will be raised.
5.751 - """
5.752 -
5.753 - FreeBusyCollectionBase.__init__(self, mutable)
5.754 - DatabaseOperations.__init__(self, column_names, filter_values, paramstyle)
5.755 - self.cursor = cursor
5.756 - self.table_name = table_name
5.757 -
5.758 - # List emulation methods.
5.759 -
5.760 - def __nonzero__(self):
5.761 - return len(self) and True or False
5.762 -
5.763 - def __iter__(self):
5.764 - query, values = self.get_query(
5.765 - "select %(columns)s from %(table)s :condition" % {
5.766 - "columns" : self.columnlist(self.period_columns),
5.767 - "table" : self.table_name
5.768 - })
5.769 - self.cursor.execute(query, values)
5.770 - return iter(map(lambda t: self.make_period(t), self.cursor.fetchall()))
5.771 -
5.772 - def __len__(self):
5.773 - query, values = self.get_query(
5.774 - "select count(*) from %(table)s :condition" % {
5.775 - "table" : self.table_name
5.776 - })
5.777 - self.cursor.execute(query, values)
5.778 - result = self.cursor.fetchone()
5.779 - return result and int(result[0]) or 0
5.780 -
5.781 - def __getitem__(self, i):
5.782 - return list(iter(self))[i]
5.783 -
5.784 - # Operations.
5.785 -
5.786 - def insert_period(self, period):
5.787 -
5.788 - "Insert the given 'period' into the collection."
5.789 -
5.790 - self._check_mutable()
5.791 -
5.792 - columns, values = self.period_columns, period.as_tuple(string_datetimes=True)
5.793 -
5.794 - query, values = self.get_query(
5.795 - "insert into %(table)s (:columns) values (:values)" % {
5.796 - "table" : self.table_name
5.797 - },
5.798 - columns, [to_string(v, "utf-8") for v in values])
5.799 -
5.800 - self.cursor.execute(query, values)
5.801 -
5.802 - def remove_periods(self, periods):
5.803 -
5.804 - "Remove the given 'periods' from the collection."
5.805 -
5.806 - self._check_mutable()
5.807 -
5.808 - for period in periods:
5.809 - values = period.as_tuple(string_datetimes=True)
5.810 -
5.811 - query, values = self.get_query(
5.812 - "delete from %(table)s :condition" % {
5.813 - "table" : self.table_name
5.814 - },
5.815 - self.period_columns, [to_string(v, "utf-8") for v in values])
5.816 -
5.817 - self.cursor.execute(query, values)
5.818 -
5.819 - def remove_event_periods(self, uid, recurrenceid=None):
5.820 -
5.821 - """
5.822 - Remove from the collection all periods associated with 'uid' and
5.823 - 'recurrenceid' (which if omitted causes the "parent" object's periods to
5.824 - be referenced).
5.825 -
5.826 - Return the removed periods.
5.827 - """
5.828 -
5.829 - self._check_mutable()
5.830 -
5.831 - if recurrenceid:
5.832 - columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid]
5.833 - else:
5.834 - columns, values = ["object_uid", "object_recurrenceid is null"], [uid]
5.835 -
5.836 - query, _values = self.get_query(
5.837 - "select %(columns)s from %(table)s :condition" % {
5.838 - "columns" : self.columnlist(self.period_columns),
5.839 - "table" : self.table_name
5.840 - },
5.841 - columns, values)
5.842 -
5.843 - self.cursor.execute(query, _values)
5.844 - removed = self.cursor.fetchall()
5.845 -
5.846 - query, values = self.get_query(
5.847 - "delete from %(table)s :condition" % {
5.848 - "table" : self.table_name
5.849 - },
5.850 - columns, values)
5.851 -
5.852 - self.cursor.execute(query, values)
5.853 -
5.854 - return map(lambda t: self.make_period(t), removed)
5.855 -
5.856 - # Specific period removal when updating event details.
5.857 -
5.858 - remove_specific_event_periods = remove_event_periods
5.859 -
5.860 - def remove_additional_periods(self, uid, recurrenceids=None):
5.861 -
5.862 - """
5.863 - Remove from the collection all periods associated with 'uid' having a
5.864 - recurrence identifier indicating an additional or modified period.
5.865 -
5.866 - If 'recurrenceids' is specified, remove all periods associated with
5.867 - 'uid' that do not have a recurrence identifier in the given list.
5.868 -
5.869 - Return the removed periods.
5.870 - """
5.871 -
5.872 - self._check_mutable()
5.873 -
5.874 - if not recurrenceids:
5.875 - columns, values = ["object_uid", "object_recurrenceid is not null"], [uid]
5.876 - else:
5.877 - columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)]
5.878 -
5.879 - query, _values = self.get_query(
5.880 - "select %(columns)s from %(table)s :condition" % {
5.881 - "columns" : self.columnlist(self.period_columns),
5.882 - "table" : self.table_name
5.883 - },
5.884 - columns, values)
5.885 -
5.886 - self.cursor.execute(query, _values)
5.887 - removed = self.cursor.fetchall()
5.888 -
5.889 - query, values = self.get_query(
5.890 - "delete from %(table)s :condition" % {
5.891 - "table" : self.table_name
5.892 - },
5.893 - columns, values)
5.894 -
5.895 - self.cursor.execute(query, values)
5.896 -
5.897 - return map(lambda t: self.make_period(t), removed)
5.898 -
5.899 - def remove_affected_period(self, uid, start):
5.900 -
5.901 - """
5.902 - Remove from the collection the period associated with 'uid' that
5.903 - provides an occurrence starting at the given 'start' (provided by a
5.904 - recurrence identifier, converted to a datetime). A recurrence identifier
5.905 - is used to provide an alternative time period whilst also acting as a
5.906 - reference to the originally-defined occurrence.
5.907 -
5.908 - Return any removed period in a list.
5.909 - """
5.910 -
5.911 - self._check_mutable()
5.912 -
5.913 - start = format_datetime(start)
5.914 -
5.915 - columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start]
5.916 -
5.917 - query, _values = self.get_query(
5.918 - "select %(columns)s from %(table)s :condition" % {
5.919 - "columns" : self.columnlist(self.period_columns),
5.920 - "table" : self.table_name
5.921 - },
5.922 - columns, values)
5.923 -
5.924 - self.cursor.execute(query, _values)
5.925 - removed = self.cursor.fetchall()
5.926 -
5.927 - query, values = self.get_query(
5.928 - "delete from %(table)s :condition" % {
5.929 - "table" : self.table_name
5.930 - },
5.931 - columns, values)
5.932 -
5.933 - self.cursor.execute(query, values)
5.934 -
5.935 - return map(lambda t: self.make_period(t), removed)
5.936 -
5.937 - def periods_from(self, period):
5.938 -
5.939 - "Return the entries in the collection at or after 'period'."
5.940 -
5.941 - start = format_datetime(period.get_start_point())
5.942 -
5.943 - columns, values = [], []
5.944 -
5.945 - if start:
5.946 - columns.append("start >= ?")
5.947 - values.append(start)
5.948 -
5.949 - query, values = self.get_query(
5.950 - "select %(columns)s from %(table)s :condition" % {
5.951 - "columns" : self.columnlist(self.period_columns),
5.952 - "table" : self.table_name
5.953 - },
5.954 - columns, values)
5.955 -
5.956 - self.cursor.execute(query, values)
5.957 -
5.958 - return map(lambda t: self.make_period(t), self.cursor.fetchall())
5.959 -
5.960 - def periods_until(self, period):
5.961 -
5.962 - "Return the entries in the collection before 'period'."
5.963 -
5.964 - end = format_datetime(period.get_end_point())
5.965 -
5.966 - columns, values = [], []
5.967 -
5.968 - if end:
5.969 - columns.append("start < ?")
5.970 - values.append(end)
5.971 -
5.972 - query, values = self.get_query(
5.973 - "select %(columns)s from %(table)s :condition" % {
5.974 - "columns" : self.columnlist(self.period_columns),
5.975 - "table" : self.table_name
5.976 - },
5.977 - columns, values)
5.978 -
5.979 - self.cursor.execute(query, values)
5.980 -
5.981 - return map(lambda t: self.make_period(t), self.cursor.fetchall())
5.982 -
5.983 - def get_overlapping(self, periods):
5.984 -
5.985 - """
5.986 - Return the entries in the collection providing periods overlapping with
5.987 - the given sorted collection of 'periods'.
5.988 - """
5.989 -
5.990 - overlapping = set()
5.991 -
5.992 - for period in periods:
5.993 - columns, values = self._get_period_values(period)
5.994 -
5.995 - query, values = self.get_query(
5.996 - "select %(columns)s from %(table)s :condition" % {
5.997 - "columns" : self.columnlist(self.period_columns),
5.998 - "table" : self.table_name
5.999 - },
5.1000 - columns, values)
5.1001 -
5.1002 - self.cursor.execute(query, values)
5.1003 -
5.1004 - overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall()))
5.1005 -
5.1006 - overlapping = list(overlapping)
5.1007 - overlapping.sort()
5.1008 - return overlapping
5.1009 -
5.1010 - def remove_overlapping(self, period):
5.1011 -
5.1012 - "Remove all periods overlapping with 'period' from the collection."
5.1013 -
5.1014 - self._check_mutable()
5.1015 -
5.1016 - columns, values = self._get_period_values(period)
5.1017 -
5.1018 - query, values = self.get_query(
5.1019 - "delete from %(table)s :condition" % {
5.1020 - "table" : self.table_name
5.1021 - },
5.1022 - columns, values)
5.1023 -
5.1024 - self.cursor.execute(query, values)
5.1025 -
5.1026 - def _get_period_values(self, period):
5.1027 -
5.1028 - start = format_datetime(period.get_start_point())
5.1029 - end = format_datetime(period.get_end_point())
5.1030 -
5.1031 - columns, values = [], []
5.1032 -
5.1033 - if end:
5.1034 - columns.append("start < ?")
5.1035 - values.append(end)
5.1036 - if start:
5.1037 - columns.append("end > ?")
5.1038 - values.append(start)
5.1039 -
5.1040 - return columns, values
5.1041 -
5.1042 -class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection):
5.1043 -
5.1044 - "A collection of quota group free/busy objects."
5.1045 -
5.1046 - def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
5.1047 -
5.1048 - """
5.1049 - Remove from the collection all periods associated with 'uid' and
5.1050 - 'recurrenceid' (which if omitted causes the "parent" object's periods to
5.1051 - be referenced) and any 'attendee'.
5.1052 -
5.1053 - Return the removed periods.
5.1054 - """
5.1055 -
5.1056 - self._check_mutable()
5.1057 -
5.1058 - columns, values = ["object_uid"], [uid]
5.1059 -
5.1060 - if recurrenceid:
5.1061 - columns.append("object_recurrenceid")
5.1062 - values.append(recurrenceid)
5.1063 - else:
5.1064 - columns.append("object_recurrenceid is null")
5.1065 -
5.1066 - if attendee:
5.1067 - columns.append("attendee")
5.1068 - values.append(attendee)
5.1069 - else:
5.1070 - columns.append("attendee is null")
5.1071 -
5.1072 - query, _values = self.get_query(
5.1073 - "select %(columns)s from %(table)s :condition" % {
5.1074 - "columns" : self.columnlist(self.period_columns),
5.1075 - "table" : self.table_name
5.1076 - },
5.1077 - columns, values)
5.1078 -
5.1079 - self.cursor.execute(query, _values)
5.1080 - removed = self.cursor.fetchall()
5.1081 -
5.1082 - query, values = self.get_query(
5.1083 - "delete from %(table)s :condition" % {
5.1084 - "table" : self.table_name
5.1085 - },
5.1086 - columns, values)
5.1087 -
5.1088 - self.cursor.execute(query, values)
5.1089 -
5.1090 - return map(lambda t: self.make_period(t), removed)
5.1091 -
5.1092 -class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection):
5.1093 -
5.1094 - "A collection of offered free/busy objects."
5.1095 -
5.1096 - pass
5.1097 -
5.1098 def get_overlapping(first, second):
5.1099
5.1100 """
6.1 --- a/imiptools/stores/database/common.py Tue May 23 16:31:27 2017 +0200
6.2 +++ b/imiptools/stores/database/common.py Tue May 23 16:34:09 2017 +0200
6.3 @@ -3,7 +3,7 @@
6.4 """
6.5 A database store of calendar data.
6.6
6.7 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
6.8 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
6.9
6.10 This program is free software; you can redistribute it and/or modify it under
6.11 the terms of the GNU General Public License as published by the Free Software
6.12 @@ -24,11 +24,13 @@
6.13 from datetime import datetime
6.14 from imiptools.data import parse_string, to_string
6.15 from imiptools.dates import format_datetime, get_datetime, to_timezone
6.16 -from imiptools.period import FreeBusyDatabaseCollection, \
6.17 - FreeBusyGroupDatabaseCollection, \
6.18 - FreeBusyOffersDatabaseCollection
6.19 +from imiptools.freebusy import FreeBusyDatabaseCollection, \
6.20 + FreeBusyGroupDatabaseCollection, \
6.21 + FreeBusyOffersDatabaseCollection
6.22 from imiptools.sql import DatabaseOperations
6.23
6.24 +def first(l): return l[0]
6.25 +
6.26 class DatabaseStoreBase(DatabaseOperations):
6.27
6.28 "A database store supporting user-specific locking."
6.29 @@ -56,6 +58,15 @@
6.30 "freebusy_provider_datetimes" : self.freebusy_provider_datetimes_table,
6.31 }
6.32
6.33 + def get_single_values(self):
6.34 +
6.35 + """
6.36 + Return the cursor results as a list of single values from each of the
6.37 + result tuples.
6.38 + """
6.39 +
6.40 + return map(first, self.cursor.fetchall())
6.41 +
6.42 class DatabaseStore(DatabaseStoreBase, StoreBase):
6.43
6.44 "A database store of tabular free/busy data and objects."
6.45 @@ -79,7 +90,7 @@
6.46 "union all select store_user from %(recurrences)s" \
6.47 ") as users")
6.48 self.cursor.execute(query)
6.49 - return [r[0] for r in self.cursor.fetchall()]
6.50 + return self.get_single_values()
6.51
6.52 # Event and event metadata access.
6.53
6.54 @@ -119,7 +130,7 @@
6.55 columns, values)
6.56
6.57 self.cursor.execute(query, values)
6.58 - return [r[0] for r in self.cursor.fetchall()]
6.59 + return self.get_single_values()
6.60
6.61 def get_cancelled_events(self, user):
6.62
6.63 @@ -505,7 +516,7 @@
6.64 columns, values)
6.65
6.66 self.cursor.execute(query, values)
6.67 - return [r[0] for r in self.cursor.fetchall()]
6.68 + return self.get_single_values()
6.69
6.70 # Tentative free/busy periods related to countering.
6.71
6.72 @@ -641,7 +652,7 @@
6.73 columns, values)
6.74
6.75 self.cursor.execute(query, values)
6.76 - return [r[0] for r in self.cursor.fetchall()]
6.77 + return self.get_single_values()
6.78
6.79 def get_counter(self, user, other, uid, recurrenceid=None):
6.80
6.81 @@ -852,7 +863,7 @@
6.82 "union all select quota from quota_limits" \
6.83 ") as quotas")
6.84 self.cursor.execute(query)
6.85 - return [r[0] for r in self.cursor.fetchall()]
6.86 + return self.get_single_values()
6.87
6.88 def get_quota_users(self, quota):
6.89
6.90 @@ -869,7 +880,7 @@
6.91 columns, values)
6.92
6.93 self.cursor.execute(query, values)
6.94 - return [r[0] for r in self.cursor.fetchall()]
6.95 + return self.get_single_values()
6.96
6.97 # Delegate information for the quota.
6.98
6.99 @@ -885,7 +896,7 @@
6.100 columns, values)
6.101
6.102 self.cursor.execute(query, values)
6.103 - return [r[0] for r in self.cursor.fetchall()]
6.104 + return self.get_single_values()
6.105
6.106 def set_delegates(self, quota, delegates):
6.107
7.1 --- a/imiptools/stores/file.py Tue May 23 16:31:27 2017 +0200
7.2 +++ b/imiptools/stores/file.py Tue May 23 16:34:09 2017 +0200
7.3 @@ -26,14 +26,16 @@
7.4 from imiptools.data import make_calendar, parse_object, to_stream
7.5 from imiptools.dates import format_datetime, get_datetime, to_timezone
7.6 from imiptools.filesys import fix_permissions, FileBase
7.7 -from imiptools.period import FreeBusyPeriod, FreeBusyGroupPeriod, \
7.8 - FreeBusyOfferPeriod, FreeBusyCollection, \
7.9 - FreeBusyGroupCollection, FreeBusyOffersCollection
7.10 +from imiptools.freebusy import FreeBusyPeriod, FreeBusyGroupPeriod, \
7.11 + FreeBusyOfferPeriod, FreeBusyCollection, \
7.12 + FreeBusyGroupCollection, FreeBusyOffersCollection
7.13 from imiptools.text import get_table, set_defaults
7.14 from os.path import isdir, isfile, join
7.15 from os import listdir, remove, rmdir
7.16 import codecs
7.17
7.18 +# Obtain defaults from the settings.
7.19 +
7.20 STORE_DIR = settings["STORE_DIR"]
7.21 PUBLISH_DIR = settings["PUBLISH_DIR"]
7.22 JOURNAL_DIR = settings["JOURNAL_DIR"]
8.1 --- a/imipweb/resource.py Tue May 23 16:31:27 2017 +0200
8.2 +++ b/imipweb/resource.py Tue May 23 16:34:09 2017 +0200
8.3 @@ -3,7 +3,7 @@
8.4 """
8.5 Common resource functionality for Web calendar clients.
8.6
8.7 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
8.8 +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
8.9
8.10 This program is free software; you can redistribute it and/or modify it under
8.11 the terms of the GNU General Public License as published by the Free Software
8.12 @@ -23,7 +23,7 @@
8.13 from imiptools.client import Client, ClientForObject
8.14 from imiptools.data import get_uri
8.15 from imiptools.dates import format_datetime, to_date
8.16 -from imiptools.period import FreeBusyCollection
8.17 +from imiptools.freebusy import FreeBusyCollection
8.18 from imipweb.data import event_period_from_period, form_period_from_period, \
8.19 FormDate, PeriodError
8.20 from imipweb.env import CGIEnvironment
9.1 --- a/tools/make_freebusy.py Tue May 23 16:31:27 2017 +0200
9.2 +++ b/tools/make_freebusy.py Tue May 23 16:34:09 2017 +0200
9.3 @@ -38,8 +38,8 @@
9.4 from imiptools.client import Client
9.5 from imiptools.data import get_window_end, Object
9.6 from imiptools.dates import get_default_timezone, to_utc_datetime
9.7 -from imiptools.period import FreeBusyCollection, FreeBusyGroupCollection, \
9.8 - FreeBusyGroupPeriod
9.9 +from imiptools.freebusy import FreeBusyCollection, FreeBusyGroupCollection, \
9.10 + FreeBusyGroupPeriod
9.11 from imiptools.stores import get_store, get_publisher, get_journal
9.12
9.13 def make_freebusy(client, participants, storage, store_and_publish,