paul@1230 | 1 | #!/usr/bin/env python |
paul@1230 | 2 | |
paul@1230 | 3 | """ |
paul@1230 | 4 | Managing free/busy periods. |
paul@1230 | 5 | |
paul@1230 | 6 | Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk> |
paul@1230 | 7 | |
paul@1230 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@1230 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@1230 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@1230 | 11 | version. |
paul@1230 | 12 | |
paul@1230 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@1230 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@1230 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@1230 | 16 | details. |
paul@1230 | 17 | |
paul@1230 | 18 | You should have received a copy of the GNU General Public License along with |
paul@1230 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@1230 | 20 | """ |
paul@1230 | 21 | |
paul@1230 | 22 | from bisect import bisect_left, bisect_right |
paul@1230 | 23 | from imiptools.dates import format_datetime |
paul@1230 | 24 | from imiptools.sql import DatabaseOperations |
paul@1230 | 25 | |
paul@1230 | 26 | def from_string(s, encoding): |
paul@1230 | 27 | if s: |
paul@1230 | 28 | return unicode(s, encoding) |
paul@1230 | 29 | else: |
paul@1230 | 30 | return s |
paul@1230 | 31 | |
paul@1230 | 32 | def to_string(s, encoding): |
paul@1230 | 33 | if s: |
paul@1230 | 34 | return s.encode(encoding) |
paul@1230 | 35 | else: |
paul@1230 | 36 | return s |
paul@1230 | 37 | |
paul@1230 | 38 | from imiptools.period import get_overlapping, Period, PeriodBase |
paul@1230 | 39 | |
paul@1230 | 40 | class FreeBusyPeriod(PeriodBase): |
paul@1230 | 41 | |
paul@1230 | 42 | "A free/busy record abstraction." |
paul@1230 | 43 | |
paul@1230 | 44 | def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, |
paul@1230 | 45 | summary=None, organiser=None): |
paul@1230 | 46 | |
paul@1230 | 47 | """ |
paul@1230 | 48 | Initialise a free/busy period with the given 'start' and 'end' points, |
paul@1230 | 49 | plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' |
paul@1230 | 50 | details. |
paul@1230 | 51 | """ |
paul@1230 | 52 | |
paul@1230 | 53 | PeriodBase.__init__(self, start, end) |
paul@1230 | 54 | self.uid = uid |
paul@1230 | 55 | self.transp = transp or None |
paul@1230 | 56 | self.recurrenceid = recurrenceid or None |
paul@1230 | 57 | self.summary = summary or None |
paul@1230 | 58 | self.organiser = organiser or None |
paul@1230 | 59 | |
paul@1230 | 60 | def as_tuple(self, strings_only=False, string_datetimes=False): |
paul@1230 | 61 | |
paul@1230 | 62 | """ |
paul@1230 | 63 | Return the initialisation parameter tuple, converting datetimes and |
paul@1230 | 64 | false value parameters to strings if 'strings_only' is set to a true |
paul@1230 | 65 | value. Otherwise, if 'string_datetimes' is set to a true value, only the |
paul@1230 | 66 | datetime values are converted to strings. |
paul@1230 | 67 | """ |
paul@1230 | 68 | |
paul@1230 | 69 | null = lambda x: (strings_only and [""] or [x])[0] |
paul@1230 | 70 | return ( |
paul@1230 | 71 | (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start, |
paul@1230 | 72 | (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end, |
paul@1230 | 73 | self.uid or null(self.uid), |
paul@1230 | 74 | self.transp or strings_only and "OPAQUE" or None, |
paul@1230 | 75 | self.recurrenceid or null(self.recurrenceid), |
paul@1230 | 76 | self.summary or null(self.summary), |
paul@1230 | 77 | self.organiser or null(self.organiser) |
paul@1230 | 78 | ) |
paul@1230 | 79 | |
paul@1230 | 80 | def __cmp__(self, other): |
paul@1230 | 81 | |
paul@1230 | 82 | """ |
paul@1230 | 83 | Compare this object to 'other', employing the uid if the periods |
paul@1230 | 84 | involved are the same. |
paul@1230 | 85 | """ |
paul@1230 | 86 | |
paul@1230 | 87 | result = PeriodBase.__cmp__(self, other) |
paul@1230 | 88 | if result == 0 and isinstance(other, FreeBusyPeriod): |
paul@1230 | 89 | return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid)) |
paul@1230 | 90 | else: |
paul@1230 | 91 | return result |
paul@1230 | 92 | |
paul@1230 | 93 | def get_key(self): |
paul@1230 | 94 | return self.uid, self.recurrenceid, self.get_start() |
paul@1230 | 95 | |
paul@1230 | 96 | def __repr__(self): |
paul@1230 | 97 | return "FreeBusyPeriod%r" % (self.as_tuple(),) |
paul@1230 | 98 | |
paul@1230 | 99 | def get_tzid(self): |
paul@1230 | 100 | return "UTC" |
paul@1230 | 101 | |
paul@1230 | 102 | # Period and event recurrence logic. |
paul@1230 | 103 | |
paul@1230 | 104 | def is_replaced(self, recurrences): |
paul@1230 | 105 | |
paul@1230 | 106 | """ |
paul@1230 | 107 | Return whether this period refers to one of the 'recurrences'. |
paul@1230 | 108 | The 'recurrences' must be UTC datetimes corresponding to the start of |
paul@1230 | 109 | the period described by a recurrence. |
paul@1230 | 110 | """ |
paul@1230 | 111 | |
paul@1230 | 112 | for recurrence in recurrences: |
paul@1230 | 113 | if self.is_affected(recurrence): |
paul@1230 | 114 | return True |
paul@1230 | 115 | return False |
paul@1230 | 116 | |
paul@1230 | 117 | def is_affected(self, recurrence): |
paul@1230 | 118 | |
paul@1230 | 119 | """ |
paul@1230 | 120 | Return whether this period refers to 'recurrence'. The 'recurrence' must |
paul@1230 | 121 | be a UTC datetime corresponding to the start of the period described by |
paul@1230 | 122 | a recurrence. |
paul@1230 | 123 | """ |
paul@1230 | 124 | |
paul@1230 | 125 | return recurrence and self.get_start_point() == recurrence |
paul@1230 | 126 | |
paul@1230 | 127 | # Value correction methods. |
paul@1230 | 128 | |
paul@1230 | 129 | def make_corrected(self, start, end): |
paul@1230 | 130 | return self.__class__(start, end) |
paul@1230 | 131 | |
paul@1230 | 132 | class FreeBusyOfferPeriod(FreeBusyPeriod): |
paul@1230 | 133 | |
paul@1230 | 134 | "A free/busy record abstraction for an offer period." |
paul@1230 | 135 | |
paul@1230 | 136 | def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, |
paul@1230 | 137 | summary=None, organiser=None, expires=None): |
paul@1230 | 138 | |
paul@1230 | 139 | """ |
paul@1230 | 140 | Initialise a free/busy period with the given 'start' and 'end' points, |
paul@1230 | 141 | plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' |
paul@1230 | 142 | details. |
paul@1230 | 143 | |
paul@1230 | 144 | An additional 'expires' parameter can be used to indicate an expiry |
paul@1230 | 145 | datetime in conjunction with free/busy offers made when countering |
paul@1230 | 146 | event proposals. |
paul@1230 | 147 | """ |
paul@1230 | 148 | |
paul@1230 | 149 | FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, |
paul@1230 | 150 | summary, organiser) |
paul@1230 | 151 | self.expires = expires or None |
paul@1230 | 152 | |
paul@1230 | 153 | def as_tuple(self, strings_only=False, string_datetimes=False): |
paul@1230 | 154 | |
paul@1230 | 155 | """ |
paul@1230 | 156 | Return the initialisation parameter tuple, converting datetimes and |
paul@1230 | 157 | false value parameters to strings if 'strings_only' is set to a true |
paul@1230 | 158 | value. Otherwise, if 'string_datetimes' is set to a true value, only the |
paul@1230 | 159 | datetime values are converted to strings. |
paul@1230 | 160 | """ |
paul@1230 | 161 | |
paul@1230 | 162 | null = lambda x: (strings_only and [""] or [x])[0] |
paul@1230 | 163 | return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( |
paul@1230 | 164 | self.expires or null(self.expires),) |
paul@1230 | 165 | |
paul@1230 | 166 | def __repr__(self): |
paul@1230 | 167 | return "FreeBusyOfferPeriod%r" % (self.as_tuple(),) |
paul@1230 | 168 | |
paul@1230 | 169 | class FreeBusyGroupPeriod(FreeBusyPeriod): |
paul@1230 | 170 | |
paul@1230 | 171 | "A free/busy record abstraction for a quota group period." |
paul@1230 | 172 | |
paul@1230 | 173 | def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, |
paul@1230 | 174 | summary=None, organiser=None, attendee=None): |
paul@1230 | 175 | |
paul@1230 | 176 | """ |
paul@1230 | 177 | Initialise a free/busy period with the given 'start' and 'end' points, |
paul@1230 | 178 | plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' |
paul@1230 | 179 | details. |
paul@1230 | 180 | |
paul@1230 | 181 | An additional 'attendee' parameter can be used to indicate the identity |
paul@1230 | 182 | of the attendee recording the period. |
paul@1230 | 183 | """ |
paul@1230 | 184 | |
paul@1230 | 185 | FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, |
paul@1230 | 186 | summary, organiser) |
paul@1230 | 187 | self.attendee = attendee or None |
paul@1230 | 188 | |
paul@1230 | 189 | def as_tuple(self, strings_only=False, string_datetimes=False): |
paul@1230 | 190 | |
paul@1230 | 191 | """ |
paul@1230 | 192 | Return the initialisation parameter tuple, converting datetimes and |
paul@1230 | 193 | false value parameters to strings if 'strings_only' is set to a true |
paul@1230 | 194 | value. Otherwise, if 'string_datetimes' is set to a true value, only the |
paul@1230 | 195 | datetime values are converted to strings. |
paul@1230 | 196 | """ |
paul@1230 | 197 | |
paul@1230 | 198 | null = lambda x: (strings_only and [""] or [x])[0] |
paul@1230 | 199 | return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( |
paul@1230 | 200 | self.attendee or null(self.attendee),) |
paul@1230 | 201 | |
paul@1230 | 202 | def __cmp__(self, other): |
paul@1230 | 203 | |
paul@1230 | 204 | """ |
paul@1230 | 205 | Compare this object to 'other', employing the uid if the periods |
paul@1230 | 206 | involved are the same. |
paul@1230 | 207 | """ |
paul@1230 | 208 | |
paul@1230 | 209 | result = FreeBusyPeriod.__cmp__(self, other) |
paul@1230 | 210 | if isinstance(other, FreeBusyGroupPeriod) and result == 0: |
paul@1230 | 211 | return cmp(self.attendee, other.attendee) |
paul@1230 | 212 | else: |
paul@1230 | 213 | return result |
paul@1230 | 214 | |
paul@1230 | 215 | def __repr__(self): |
paul@1230 | 216 | return "FreeBusyGroupPeriod%r" % (self.as_tuple(),) |
paul@1230 | 217 | |
paul@1230 | 218 | class FreeBusyCollectionBase: |
paul@1230 | 219 | |
paul@1230 | 220 | "Common operations on free/busy period collections." |
paul@1230 | 221 | |
paul@1230 | 222 | period_columns = [ |
paul@1230 | 223 | "start", "end", "object_uid", "transp", "object_recurrenceid", |
paul@1230 | 224 | "summary", "organiser" |
paul@1230 | 225 | ] |
paul@1230 | 226 | |
paul@1230 | 227 | period_class = FreeBusyPeriod |
paul@1230 | 228 | |
paul@1230 | 229 | def __init__(self, mutable=True): |
paul@1230 | 230 | self.mutable = mutable |
paul@1230 | 231 | |
paul@1230 | 232 | def _check_mutable(self): |
paul@1230 | 233 | if not self.mutable: |
paul@1230 | 234 | raise TypeError, "Cannot mutate this collection." |
paul@1230 | 235 | |
paul@1230 | 236 | def copy(self): |
paul@1230 | 237 | |
paul@1230 | 238 | "Make an independent mutable copy of the collection." |
paul@1230 | 239 | |
paul@1230 | 240 | return FreeBusyCollection(list(self), True) |
paul@1230 | 241 | |
paul@1230 | 242 | def make_period(self, t): |
paul@1230 | 243 | |
paul@1230 | 244 | """ |
paul@1230 | 245 | Make a period using the given tuple of arguments and the collection's |
paul@1230 | 246 | column details. |
paul@1230 | 247 | """ |
paul@1230 | 248 | |
paul@1230 | 249 | args = [] |
paul@1230 | 250 | for arg, column in zip(t, self.period_columns): |
paul@1230 | 251 | args.append(from_string(arg, "utf-8")) |
paul@1230 | 252 | return self.period_class(*args) |
paul@1230 | 253 | |
paul@1230 | 254 | def make_tuple(self, t): |
paul@1230 | 255 | |
paul@1230 | 256 | """ |
paul@1230 | 257 | Return a tuple from the given tuple 't' conforming to the collection's |
paul@1230 | 258 | column details. |
paul@1230 | 259 | """ |
paul@1230 | 260 | |
paul@1230 | 261 | args = [] |
paul@1230 | 262 | for arg, column in zip(t, self.period_columns): |
paul@1230 | 263 | args.append(arg) |
paul@1230 | 264 | return tuple(args) |
paul@1230 | 265 | |
paul@1230 | 266 | # List emulation methods. |
paul@1230 | 267 | |
paul@1230 | 268 | def __iadd__(self, periods): |
paul@1230 | 269 | for period in periods: |
paul@1230 | 270 | self.insert_period(period) |
paul@1230 | 271 | return self |
paul@1230 | 272 | |
paul@1230 | 273 | def append(self, period): |
paul@1230 | 274 | self.insert_period(period) |
paul@1230 | 275 | |
paul@1230 | 276 | # Operations. |
paul@1230 | 277 | |
paul@1230 | 278 | def can_schedule(self, periods, uid, recurrenceid): |
paul@1230 | 279 | |
paul@1230 | 280 | """ |
paul@1230 | 281 | Return whether the collection can accommodate the given 'periods' |
paul@1230 | 282 | employing the specified 'uid' and 'recurrenceid'. |
paul@1230 | 283 | """ |
paul@1230 | 284 | |
paul@1230 | 285 | for conflict in self.have_conflict(periods, True): |
paul@1230 | 286 | if conflict.uid != uid or conflict.recurrenceid != recurrenceid: |
paul@1230 | 287 | return False |
paul@1230 | 288 | |
paul@1230 | 289 | return True |
paul@1230 | 290 | |
paul@1230 | 291 | def have_conflict(self, periods, get_conflicts=False): |
paul@1230 | 292 | |
paul@1230 | 293 | """ |
paul@1230 | 294 | Return whether any period in the collection overlaps with the given |
paul@1230 | 295 | 'periods', returning a collection of such overlapping periods if |
paul@1230 | 296 | 'get_conflicts' is set to a true value. |
paul@1230 | 297 | """ |
paul@1230 | 298 | |
paul@1230 | 299 | conflicts = set() |
paul@1230 | 300 | for p in periods: |
paul@1230 | 301 | overlapping = self.period_overlaps(p, get_conflicts) |
paul@1230 | 302 | if overlapping: |
paul@1230 | 303 | if get_conflicts: |
paul@1230 | 304 | conflicts.update(overlapping) |
paul@1230 | 305 | else: |
paul@1230 | 306 | return True |
paul@1230 | 307 | |
paul@1230 | 308 | if get_conflicts: |
paul@1230 | 309 | return conflicts |
paul@1230 | 310 | else: |
paul@1230 | 311 | return False |
paul@1230 | 312 | |
paul@1230 | 313 | def period_overlaps(self, period, get_periods=False): |
paul@1230 | 314 | |
paul@1230 | 315 | """ |
paul@1230 | 316 | Return whether any period in the collection overlaps with the given |
paul@1230 | 317 | 'period', returning a collection of overlapping periods if 'get_periods' |
paul@1230 | 318 | is set to a true value. |
paul@1230 | 319 | """ |
paul@1230 | 320 | |
paul@1230 | 321 | overlapping = self.get_overlapping([period]) |
paul@1230 | 322 | |
paul@1230 | 323 | if get_periods: |
paul@1230 | 324 | return overlapping |
paul@1230 | 325 | else: |
paul@1230 | 326 | return len(overlapping) != 0 |
paul@1230 | 327 | |
paul@1230 | 328 | def replace_overlapping(self, period, replacements): |
paul@1230 | 329 | |
paul@1230 | 330 | """ |
paul@1230 | 331 | Replace existing periods in the collection within the given 'period', |
paul@1230 | 332 | using the given 'replacements'. |
paul@1230 | 333 | """ |
paul@1230 | 334 | |
paul@1230 | 335 | self._check_mutable() |
paul@1230 | 336 | |
paul@1230 | 337 | self.remove_overlapping(period) |
paul@1230 | 338 | for replacement in replacements: |
paul@1230 | 339 | self.insert_period(replacement) |
paul@1230 | 340 | |
paul@1230 | 341 | def coalesce_freebusy(self): |
paul@1230 | 342 | |
paul@1230 | 343 | "Coalesce the periods in the collection, returning a new collection." |
paul@1230 | 344 | |
paul@1230 | 345 | if not self: |
paul@1230 | 346 | return FreeBusyCollection() |
paul@1230 | 347 | |
paul@1230 | 348 | fb = [] |
paul@1230 | 349 | |
paul@1230 | 350 | it = iter(self) |
paul@1230 | 351 | period = it.next() |
paul@1230 | 352 | |
paul@1230 | 353 | start = period.get_start_point() |
paul@1230 | 354 | end = period.get_end_point() |
paul@1230 | 355 | |
paul@1230 | 356 | try: |
paul@1230 | 357 | while True: |
paul@1230 | 358 | period = it.next() |
paul@1230 | 359 | if period.get_start_point() > end: |
paul@1230 | 360 | fb.append(self.period_class(start, end)) |
paul@1230 | 361 | start = period.get_start_point() |
paul@1230 | 362 | end = period.get_end_point() |
paul@1230 | 363 | else: |
paul@1230 | 364 | end = max(end, period.get_end_point()) |
paul@1230 | 365 | except StopIteration: |
paul@1230 | 366 | pass |
paul@1230 | 367 | |
paul@1230 | 368 | fb.append(self.period_class(start, end)) |
paul@1230 | 369 | return FreeBusyCollection(fb) |
paul@1230 | 370 | |
paul@1230 | 371 | def invert_freebusy(self): |
paul@1230 | 372 | |
paul@1230 | 373 | "Return the free periods from the collection as a new collection." |
paul@1230 | 374 | |
paul@1230 | 375 | if not self: |
paul@1230 | 376 | return FreeBusyCollection([self.period_class(None, None)]) |
paul@1230 | 377 | |
paul@1230 | 378 | # Coalesce periods that overlap or are adjacent. |
paul@1230 | 379 | |
paul@1230 | 380 | fb = self.coalesce_freebusy() |
paul@1230 | 381 | free = [] |
paul@1230 | 382 | |
paul@1230 | 383 | # Add a start-of-time period if appropriate. |
paul@1230 | 384 | |
paul@1230 | 385 | first = fb[0].get_start_point() |
paul@1230 | 386 | if first: |
paul@1230 | 387 | free.append(self.period_class(None, first)) |
paul@1230 | 388 | |
paul@1230 | 389 | start = fb[0].get_end_point() |
paul@1230 | 390 | |
paul@1230 | 391 | for period in fb[1:]: |
paul@1230 | 392 | free.append(self.period_class(start, period.get_start_point())) |
paul@1230 | 393 | start = period.get_end_point() |
paul@1230 | 394 | |
paul@1230 | 395 | # Add an end-of-time period if appropriate. |
paul@1230 | 396 | |
paul@1230 | 397 | if start: |
paul@1230 | 398 | free.append(self.period_class(start, None)) |
paul@1230 | 399 | |
paul@1230 | 400 | return FreeBusyCollection(free) |
paul@1230 | 401 | |
paul@1230 | 402 | def _update_freebusy(self, periods, uid, recurrenceid): |
paul@1230 | 403 | |
paul@1230 | 404 | """ |
paul@1230 | 405 | Update the free/busy details with the given 'periods', using the given |
paul@1230 | 406 | 'uid' plus 'recurrenceid' to remove existing periods. |
paul@1230 | 407 | """ |
paul@1230 | 408 | |
paul@1230 | 409 | self._check_mutable() |
paul@1230 | 410 | |
paul@1230 | 411 | self.remove_specific_event_periods(uid, recurrenceid) |
paul@1230 | 412 | |
paul@1230 | 413 | for p in periods: |
paul@1230 | 414 | self.insert_period(p) |
paul@1230 | 415 | |
paul@1230 | 416 | def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser): |
paul@1230 | 417 | |
paul@1230 | 418 | """ |
paul@1230 | 419 | Update the free/busy details with the given 'periods', 'transp' setting, |
paul@1230 | 420 | 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. |
paul@1230 | 421 | """ |
paul@1230 | 422 | |
paul@1230 | 423 | new_periods = [] |
paul@1230 | 424 | |
paul@1230 | 425 | for p in periods: |
paul@1230 | 426 | new_periods.append( |
paul@1230 | 427 | self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser) |
paul@1230 | 428 | ) |
paul@1230 | 429 | |
paul@1230 | 430 | self._update_freebusy(new_periods, uid, recurrenceid) |
paul@1230 | 431 | |
paul@1230 | 432 | class SupportAttendee: |
paul@1230 | 433 | |
paul@1230 | 434 | "A mix-in that supports the affected attendee in free/busy periods." |
paul@1230 | 435 | |
paul@1230 | 436 | period_columns = FreeBusyCollectionBase.period_columns + ["attendee"] |
paul@1230 | 437 | period_class = FreeBusyGroupPeriod |
paul@1230 | 438 | |
paul@1230 | 439 | def _update_freebusy(self, periods, uid, recurrenceid, attendee=None): |
paul@1230 | 440 | |
paul@1230 | 441 | """ |
paul@1230 | 442 | Update the free/busy details with the given 'periods', using the given |
paul@1230 | 443 | 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods. |
paul@1230 | 444 | """ |
paul@1230 | 445 | |
paul@1230 | 446 | self._check_mutable() |
paul@1230 | 447 | |
paul@1230 | 448 | self.remove_specific_event_periods(uid, recurrenceid, attendee) |
paul@1230 | 449 | |
paul@1230 | 450 | for p in periods: |
paul@1230 | 451 | self.insert_period(p) |
paul@1230 | 452 | |
paul@1230 | 453 | def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None): |
paul@1230 | 454 | |
paul@1230 | 455 | """ |
paul@1230 | 456 | Update the free/busy details with the given 'periods', 'transp' setting, |
paul@1230 | 457 | 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. |
paul@1230 | 458 | |
paul@1230 | 459 | An optional 'attendee' indicates the attendee affected by the period. |
paul@1230 | 460 | """ |
paul@1230 | 461 | |
paul@1230 | 462 | new_periods = [] |
paul@1230 | 463 | |
paul@1230 | 464 | for p in periods: |
paul@1230 | 465 | new_periods.append( |
paul@1230 | 466 | self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee) |
paul@1230 | 467 | ) |
paul@1230 | 468 | |
paul@1230 | 469 | self._update_freebusy(new_periods, uid, recurrenceid, attendee) |
paul@1230 | 470 | |
paul@1230 | 471 | class SupportExpires: |
paul@1230 | 472 | |
paul@1230 | 473 | "A mix-in that supports the expiry datetime in free/busy periods." |
paul@1230 | 474 | |
paul@1230 | 475 | period_columns = FreeBusyCollectionBase.period_columns + ["expires"] |
paul@1230 | 476 | period_class = FreeBusyOfferPeriod |
paul@1230 | 477 | |
paul@1230 | 478 | def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None): |
paul@1230 | 479 | |
paul@1230 | 480 | """ |
paul@1230 | 481 | Update the free/busy details with the given 'periods', 'transp' setting, |
paul@1230 | 482 | 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. |
paul@1230 | 483 | |
paul@1230 | 484 | An optional 'expires' datetime string indicates the expiry time of any |
paul@1230 | 485 | free/busy offer. |
paul@1230 | 486 | """ |
paul@1230 | 487 | |
paul@1230 | 488 | new_periods = [] |
paul@1230 | 489 | |
paul@1230 | 490 | for p in periods: |
paul@1230 | 491 | new_periods.append( |
paul@1230 | 492 | self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires) |
paul@1230 | 493 | ) |
paul@1230 | 494 | |
paul@1230 | 495 | self._update_freebusy(new_periods, uid, recurrenceid) |
paul@1230 | 496 | |
paul@1230 | 497 | class FreeBusyCollection(FreeBusyCollectionBase): |
paul@1230 | 498 | |
paul@1230 | 499 | "An abstraction for a collection of free/busy periods." |
paul@1230 | 500 | |
paul@1230 | 501 | def __init__(self, periods=None, mutable=True): |
paul@1230 | 502 | |
paul@1230 | 503 | """ |
paul@1230 | 504 | Initialise the collection with the given list of 'periods', or start an |
paul@1230 | 505 | empty collection if no list is given. If 'mutable' is indicated, the |
paul@1230 | 506 | collection may be changed; otherwise, an exception will be raised. |
paul@1230 | 507 | """ |
paul@1230 | 508 | |
paul@1230 | 509 | FreeBusyCollectionBase.__init__(self, mutable) |
paul@1230 | 510 | self.periods = periods or [] |
paul@1230 | 511 | |
paul@1230 | 512 | # List emulation methods. |
paul@1230 | 513 | |
paul@1230 | 514 | def __nonzero__(self): |
paul@1230 | 515 | return bool(self.periods) |
paul@1230 | 516 | |
paul@1230 | 517 | def __iter__(self): |
paul@1230 | 518 | return iter(self.periods) |
paul@1230 | 519 | |
paul@1230 | 520 | def __len__(self): |
paul@1230 | 521 | return len(self.periods) |
paul@1230 | 522 | |
paul@1230 | 523 | def __getitem__(self, i): |
paul@1230 | 524 | return self.periods[i] |
paul@1230 | 525 | |
paul@1230 | 526 | # Operations. |
paul@1230 | 527 | |
paul@1230 | 528 | def insert_period(self, period): |
paul@1230 | 529 | |
paul@1230 | 530 | "Insert the given 'period' into the collection." |
paul@1230 | 531 | |
paul@1230 | 532 | self._check_mutable() |
paul@1230 | 533 | |
paul@1230 | 534 | i = bisect_left(self.periods, period) |
paul@1230 | 535 | if i == len(self.periods): |
paul@1230 | 536 | self.periods.append(period) |
paul@1230 | 537 | elif self.periods[i] != period: |
paul@1230 | 538 | self.periods.insert(i, period) |
paul@1230 | 539 | |
paul@1230 | 540 | def remove_periods(self, periods): |
paul@1230 | 541 | |
paul@1230 | 542 | "Remove the given 'periods' from the collection." |
paul@1230 | 543 | |
paul@1230 | 544 | self._check_mutable() |
paul@1230 | 545 | |
paul@1230 | 546 | for period in periods: |
paul@1230 | 547 | i = bisect_left(self.periods, period) |
paul@1230 | 548 | if i < len(self.periods) and self.periods[i] == period: |
paul@1230 | 549 | del self.periods[i] |
paul@1230 | 550 | |
paul@1230 | 551 | def remove_event_periods(self, uid, recurrenceid=None): |
paul@1230 | 552 | |
paul@1230 | 553 | """ |
paul@1230 | 554 | Remove from the collection all periods associated with 'uid' and |
paul@1230 | 555 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1230 | 556 | be referenced). |
paul@1230 | 557 | |
paul@1230 | 558 | Return the removed periods. |
paul@1230 | 559 | """ |
paul@1230 | 560 | |
paul@1230 | 561 | self._check_mutable() |
paul@1230 | 562 | |
paul@1230 | 563 | removed = [] |
paul@1230 | 564 | i = 0 |
paul@1230 | 565 | while i < len(self.periods): |
paul@1230 | 566 | fb = self.periods[i] |
paul@1230 | 567 | if fb.uid == uid and fb.recurrenceid == recurrenceid: |
paul@1230 | 568 | removed.append(self.periods[i]) |
paul@1230 | 569 | del self.periods[i] |
paul@1230 | 570 | else: |
paul@1230 | 571 | i += 1 |
paul@1230 | 572 | |
paul@1230 | 573 | return removed |
paul@1230 | 574 | |
paul@1230 | 575 | # Specific period removal when updating event details. |
paul@1230 | 576 | |
paul@1230 | 577 | remove_specific_event_periods = remove_event_periods |
paul@1230 | 578 | |
paul@1230 | 579 | def remove_additional_periods(self, uid, recurrenceids=None): |
paul@1230 | 580 | |
paul@1230 | 581 | """ |
paul@1230 | 582 | Remove from the collection all periods associated with 'uid' having a |
paul@1230 | 583 | recurrence identifier indicating an additional or modified period. |
paul@1230 | 584 | |
paul@1230 | 585 | If 'recurrenceids' is specified, remove all periods associated with |
paul@1230 | 586 | 'uid' that do not have a recurrence identifier in the given list. |
paul@1230 | 587 | |
paul@1230 | 588 | Return the removed periods. |
paul@1230 | 589 | """ |
paul@1230 | 590 | |
paul@1230 | 591 | self._check_mutable() |
paul@1230 | 592 | |
paul@1230 | 593 | removed = [] |
paul@1230 | 594 | i = 0 |
paul@1230 | 595 | while i < len(self.periods): |
paul@1230 | 596 | fb = self.periods[i] |
paul@1230 | 597 | if fb.uid == uid and fb.recurrenceid and ( |
paul@1230 | 598 | recurrenceids is None or |
paul@1230 | 599 | recurrenceids is not None and fb.recurrenceid not in recurrenceids |
paul@1230 | 600 | ): |
paul@1230 | 601 | removed.append(self.periods[i]) |
paul@1230 | 602 | del self.periods[i] |
paul@1230 | 603 | else: |
paul@1230 | 604 | i += 1 |
paul@1230 | 605 | |
paul@1230 | 606 | return removed |
paul@1230 | 607 | |
paul@1230 | 608 | def remove_affected_period(self, uid, start): |
paul@1230 | 609 | |
paul@1230 | 610 | """ |
paul@1230 | 611 | Remove from the collection the period associated with 'uid' that |
paul@1230 | 612 | provides an occurrence starting at the given 'start' (provided by a |
paul@1230 | 613 | recurrence identifier, converted to a datetime). A recurrence identifier |
paul@1230 | 614 | is used to provide an alternative time period whilst also acting as a |
paul@1230 | 615 | reference to the originally-defined occurrence. |
paul@1230 | 616 | |
paul@1230 | 617 | Return any removed period in a list. |
paul@1230 | 618 | """ |
paul@1230 | 619 | |
paul@1230 | 620 | self._check_mutable() |
paul@1230 | 621 | |
paul@1230 | 622 | removed = [] |
paul@1230 | 623 | |
paul@1230 | 624 | search = Period(start, start) |
paul@1230 | 625 | found = bisect_left(self.periods, search) |
paul@1230 | 626 | |
paul@1230 | 627 | while found < len(self.periods): |
paul@1230 | 628 | fb = self.periods[found] |
paul@1230 | 629 | |
paul@1230 | 630 | # Stop looking if the start no longer matches the recurrence identifier. |
paul@1230 | 631 | |
paul@1230 | 632 | if fb.get_start_point() != search.get_start_point(): |
paul@1230 | 633 | break |
paul@1230 | 634 | |
paul@1230 | 635 | # If the period belongs to the parent object, remove it and return. |
paul@1230 | 636 | |
paul@1230 | 637 | if not fb.recurrenceid and uid == fb.uid: |
paul@1230 | 638 | removed.append(self.periods[found]) |
paul@1230 | 639 | del self.periods[found] |
paul@1230 | 640 | break |
paul@1230 | 641 | |
paul@1230 | 642 | # Otherwise, keep looking for a matching period. |
paul@1230 | 643 | |
paul@1230 | 644 | found += 1 |
paul@1230 | 645 | |
paul@1230 | 646 | return removed |
paul@1230 | 647 | |
paul@1230 | 648 | def periods_from(self, period): |
paul@1230 | 649 | |
paul@1230 | 650 | "Return the entries in the collection at or after 'period'." |
paul@1230 | 651 | |
paul@1230 | 652 | first = bisect_left(self.periods, period) |
paul@1230 | 653 | return self.periods[first:] |
paul@1230 | 654 | |
paul@1230 | 655 | def periods_until(self, period): |
paul@1230 | 656 | |
paul@1230 | 657 | "Return the entries in the collection before 'period'." |
paul@1230 | 658 | |
paul@1230 | 659 | last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid())) |
paul@1230 | 660 | return self.periods[:last] |
paul@1230 | 661 | |
paul@1230 | 662 | def get_overlapping(self, periods): |
paul@1230 | 663 | |
paul@1230 | 664 | """ |
paul@1230 | 665 | Return the entries in the collection providing periods overlapping with |
paul@1230 | 666 | the given sorted collection of 'periods'. |
paul@1230 | 667 | """ |
paul@1230 | 668 | |
paul@1230 | 669 | return get_overlapping(self.periods, periods) |
paul@1230 | 670 | |
paul@1230 | 671 | def remove_overlapping(self, period): |
paul@1230 | 672 | |
paul@1230 | 673 | "Remove all periods overlapping with 'period' from the collection." |
paul@1230 | 674 | |
paul@1230 | 675 | self._check_mutable() |
paul@1230 | 676 | |
paul@1230 | 677 | overlapping = self.get_overlapping([period]) |
paul@1230 | 678 | |
paul@1230 | 679 | if overlapping: |
paul@1230 | 680 | for fb in overlapping: |
paul@1230 | 681 | self.periods.remove(fb) |
paul@1230 | 682 | |
paul@1230 | 683 | class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection): |
paul@1230 | 684 | |
paul@1230 | 685 | "A collection of quota group free/busy objects." |
paul@1230 | 686 | |
paul@1230 | 687 | def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): |
paul@1230 | 688 | |
paul@1230 | 689 | """ |
paul@1230 | 690 | Remove from the collection all periods associated with 'uid' and |
paul@1230 | 691 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1230 | 692 | be referenced) and any 'attendee'. |
paul@1230 | 693 | |
paul@1230 | 694 | Return the removed periods. |
paul@1230 | 695 | """ |
paul@1230 | 696 | |
paul@1230 | 697 | self._check_mutable() |
paul@1230 | 698 | |
paul@1230 | 699 | removed = [] |
paul@1230 | 700 | i = 0 |
paul@1230 | 701 | while i < len(self.periods): |
paul@1230 | 702 | fb = self.periods[i] |
paul@1230 | 703 | if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee: |
paul@1230 | 704 | removed.append(self.periods[i]) |
paul@1230 | 705 | del self.periods[i] |
paul@1230 | 706 | else: |
paul@1230 | 707 | i += 1 |
paul@1230 | 708 | |
paul@1230 | 709 | return removed |
paul@1230 | 710 | |
paul@1230 | 711 | class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection): |
paul@1230 | 712 | |
paul@1230 | 713 | "A collection of offered free/busy objects." |
paul@1230 | 714 | |
paul@1230 | 715 | pass |
paul@1230 | 716 | |
paul@1230 | 717 | class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations): |
paul@1230 | 718 | |
paul@1230 | 719 | """ |
paul@1230 | 720 | An abstraction for a collection of free/busy periods stored in a database |
paul@1230 | 721 | system. |
paul@1230 | 722 | """ |
paul@1230 | 723 | |
paul@1230 | 724 | def __init__(self, cursor, table_name, column_names=None, filter_values=None, |
paul@1230 | 725 | mutable=True, paramstyle=None): |
paul@1230 | 726 | |
paul@1230 | 727 | """ |
paul@1230 | 728 | Initialise the collection with the given 'cursor' and with the |
paul@1230 | 729 | 'table_name', 'column_names' and 'filter_values' configuring the |
paul@1230 | 730 | selection of data. If 'mutable' is indicated, the collection may be |
paul@1230 | 731 | changed; otherwise, an exception will be raised. |
paul@1230 | 732 | """ |
paul@1230 | 733 | |
paul@1230 | 734 | FreeBusyCollectionBase.__init__(self, mutable) |
paul@1230 | 735 | DatabaseOperations.__init__(self, column_names, filter_values, paramstyle) |
paul@1230 | 736 | self.cursor = cursor |
paul@1230 | 737 | self.table_name = table_name |
paul@1230 | 738 | |
paul@1230 | 739 | # List emulation methods. |
paul@1230 | 740 | |
paul@1230 | 741 | def __nonzero__(self): |
paul@1230 | 742 | return len(self) and True or False |
paul@1230 | 743 | |
paul@1230 | 744 | def __iter__(self): |
paul@1230 | 745 | query, values = self.get_query( |
paul@1230 | 746 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 747 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 748 | "table" : self.table_name |
paul@1230 | 749 | }) |
paul@1230 | 750 | self.cursor.execute(query, values) |
paul@1230 | 751 | return iter(map(lambda t: self.make_period(t), self.cursor.fetchall())) |
paul@1230 | 752 | |
paul@1230 | 753 | def __len__(self): |
paul@1230 | 754 | query, values = self.get_query( |
paul@1230 | 755 | "select count(*) from %(table)s :condition" % { |
paul@1230 | 756 | "table" : self.table_name |
paul@1230 | 757 | }) |
paul@1230 | 758 | self.cursor.execute(query, values) |
paul@1230 | 759 | result = self.cursor.fetchone() |
paul@1230 | 760 | return result and int(result[0]) or 0 |
paul@1230 | 761 | |
paul@1230 | 762 | def __getitem__(self, i): |
paul@1230 | 763 | return list(iter(self))[i] |
paul@1230 | 764 | |
paul@1230 | 765 | # Operations. |
paul@1230 | 766 | |
paul@1230 | 767 | def insert_period(self, period): |
paul@1230 | 768 | |
paul@1230 | 769 | "Insert the given 'period' into the collection." |
paul@1230 | 770 | |
paul@1230 | 771 | self._check_mutable() |
paul@1230 | 772 | |
paul@1230 | 773 | columns, values = self.period_columns, period.as_tuple(string_datetimes=True) |
paul@1230 | 774 | |
paul@1230 | 775 | query, values = self.get_query( |
paul@1230 | 776 | "insert into %(table)s (:columns) values (:values)" % { |
paul@1230 | 777 | "table" : self.table_name |
paul@1230 | 778 | }, |
paul@1230 | 779 | columns, [to_string(v, "utf-8") for v in values]) |
paul@1230 | 780 | |
paul@1230 | 781 | self.cursor.execute(query, values) |
paul@1230 | 782 | |
paul@1230 | 783 | def remove_periods(self, periods): |
paul@1230 | 784 | |
paul@1230 | 785 | "Remove the given 'periods' from the collection." |
paul@1230 | 786 | |
paul@1230 | 787 | self._check_mutable() |
paul@1230 | 788 | |
paul@1230 | 789 | for period in periods: |
paul@1230 | 790 | values = period.as_tuple(string_datetimes=True) |
paul@1230 | 791 | |
paul@1230 | 792 | query, values = self.get_query( |
paul@1230 | 793 | "delete from %(table)s :condition" % { |
paul@1230 | 794 | "table" : self.table_name |
paul@1230 | 795 | }, |
paul@1230 | 796 | self.period_columns, [to_string(v, "utf-8") for v in values]) |
paul@1230 | 797 | |
paul@1230 | 798 | self.cursor.execute(query, values) |
paul@1230 | 799 | |
paul@1230 | 800 | def remove_event_periods(self, uid, recurrenceid=None): |
paul@1230 | 801 | |
paul@1230 | 802 | """ |
paul@1230 | 803 | Remove from the collection all periods associated with 'uid' and |
paul@1230 | 804 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1230 | 805 | be referenced). |
paul@1230 | 806 | |
paul@1230 | 807 | Return the removed periods. |
paul@1230 | 808 | """ |
paul@1230 | 809 | |
paul@1230 | 810 | self._check_mutable() |
paul@1230 | 811 | |
paul@1230 | 812 | if recurrenceid: |
paul@1230 | 813 | columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid] |
paul@1230 | 814 | else: |
paul@1230 | 815 | columns, values = ["object_uid", "object_recurrenceid is null"], [uid] |
paul@1230 | 816 | |
paul@1230 | 817 | query, _values = self.get_query( |
paul@1230 | 818 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 819 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 820 | "table" : self.table_name |
paul@1230 | 821 | }, |
paul@1230 | 822 | columns, values) |
paul@1230 | 823 | |
paul@1230 | 824 | self.cursor.execute(query, _values) |
paul@1230 | 825 | removed = self.cursor.fetchall() |
paul@1230 | 826 | |
paul@1230 | 827 | query, values = self.get_query( |
paul@1230 | 828 | "delete from %(table)s :condition" % { |
paul@1230 | 829 | "table" : self.table_name |
paul@1230 | 830 | }, |
paul@1230 | 831 | columns, values) |
paul@1230 | 832 | |
paul@1230 | 833 | self.cursor.execute(query, values) |
paul@1230 | 834 | |
paul@1230 | 835 | return map(lambda t: self.make_period(t), removed) |
paul@1230 | 836 | |
paul@1230 | 837 | # Specific period removal when updating event details. |
paul@1230 | 838 | |
paul@1230 | 839 | remove_specific_event_periods = remove_event_periods |
paul@1230 | 840 | |
paul@1230 | 841 | def remove_additional_periods(self, uid, recurrenceids=None): |
paul@1230 | 842 | |
paul@1230 | 843 | """ |
paul@1230 | 844 | Remove from the collection all periods associated with 'uid' having a |
paul@1230 | 845 | recurrence identifier indicating an additional or modified period. |
paul@1230 | 846 | |
paul@1230 | 847 | If 'recurrenceids' is specified, remove all periods associated with |
paul@1230 | 848 | 'uid' that do not have a recurrence identifier in the given list. |
paul@1230 | 849 | |
paul@1230 | 850 | Return the removed periods. |
paul@1230 | 851 | """ |
paul@1230 | 852 | |
paul@1230 | 853 | self._check_mutable() |
paul@1230 | 854 | |
paul@1230 | 855 | if not recurrenceids: |
paul@1230 | 856 | columns, values = ["object_uid", "object_recurrenceid is not null"], [uid] |
paul@1230 | 857 | else: |
paul@1230 | 858 | columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)] |
paul@1230 | 859 | |
paul@1230 | 860 | query, _values = self.get_query( |
paul@1230 | 861 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 862 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 863 | "table" : self.table_name |
paul@1230 | 864 | }, |
paul@1230 | 865 | columns, values) |
paul@1230 | 866 | |
paul@1230 | 867 | self.cursor.execute(query, _values) |
paul@1230 | 868 | removed = self.cursor.fetchall() |
paul@1230 | 869 | |
paul@1230 | 870 | query, values = self.get_query( |
paul@1230 | 871 | "delete from %(table)s :condition" % { |
paul@1230 | 872 | "table" : self.table_name |
paul@1230 | 873 | }, |
paul@1230 | 874 | columns, values) |
paul@1230 | 875 | |
paul@1230 | 876 | self.cursor.execute(query, values) |
paul@1230 | 877 | |
paul@1230 | 878 | return map(lambda t: self.make_period(t), removed) |
paul@1230 | 879 | |
paul@1230 | 880 | def remove_affected_period(self, uid, start): |
paul@1230 | 881 | |
paul@1230 | 882 | """ |
paul@1230 | 883 | Remove from the collection the period associated with 'uid' that |
paul@1230 | 884 | provides an occurrence starting at the given 'start' (provided by a |
paul@1230 | 885 | recurrence identifier, converted to a datetime). A recurrence identifier |
paul@1230 | 886 | is used to provide an alternative time period whilst also acting as a |
paul@1230 | 887 | reference to the originally-defined occurrence. |
paul@1230 | 888 | |
paul@1230 | 889 | Return any removed period in a list. |
paul@1230 | 890 | """ |
paul@1230 | 891 | |
paul@1230 | 892 | self._check_mutable() |
paul@1230 | 893 | |
paul@1230 | 894 | start = format_datetime(start) |
paul@1230 | 895 | |
paul@1230 | 896 | columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start] |
paul@1230 | 897 | |
paul@1230 | 898 | query, _values = self.get_query( |
paul@1230 | 899 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 900 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 901 | "table" : self.table_name |
paul@1230 | 902 | }, |
paul@1230 | 903 | columns, values) |
paul@1230 | 904 | |
paul@1230 | 905 | self.cursor.execute(query, _values) |
paul@1230 | 906 | removed = self.cursor.fetchall() |
paul@1230 | 907 | |
paul@1230 | 908 | query, values = self.get_query( |
paul@1230 | 909 | "delete from %(table)s :condition" % { |
paul@1230 | 910 | "table" : self.table_name |
paul@1230 | 911 | }, |
paul@1230 | 912 | columns, values) |
paul@1230 | 913 | |
paul@1230 | 914 | self.cursor.execute(query, values) |
paul@1230 | 915 | |
paul@1230 | 916 | return map(lambda t: self.make_period(t), removed) |
paul@1230 | 917 | |
paul@1230 | 918 | def periods_from(self, period): |
paul@1230 | 919 | |
paul@1230 | 920 | "Return the entries in the collection at or after 'period'." |
paul@1230 | 921 | |
paul@1230 | 922 | start = format_datetime(period.get_start_point()) |
paul@1230 | 923 | |
paul@1230 | 924 | columns, values = [], [] |
paul@1230 | 925 | |
paul@1230 | 926 | if start: |
paul@1230 | 927 | columns.append("start >= ?") |
paul@1230 | 928 | values.append(start) |
paul@1230 | 929 | |
paul@1230 | 930 | query, values = self.get_query( |
paul@1230 | 931 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 932 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 933 | "table" : self.table_name |
paul@1230 | 934 | }, |
paul@1230 | 935 | columns, values) |
paul@1230 | 936 | |
paul@1230 | 937 | self.cursor.execute(query, values) |
paul@1230 | 938 | |
paul@1230 | 939 | return map(lambda t: self.make_period(t), self.cursor.fetchall()) |
paul@1230 | 940 | |
paul@1230 | 941 | def periods_until(self, period): |
paul@1230 | 942 | |
paul@1230 | 943 | "Return the entries in the collection before 'period'." |
paul@1230 | 944 | |
paul@1230 | 945 | end = format_datetime(period.get_end_point()) |
paul@1230 | 946 | |
paul@1230 | 947 | columns, values = [], [] |
paul@1230 | 948 | |
paul@1230 | 949 | if end: |
paul@1230 | 950 | columns.append("start < ?") |
paul@1230 | 951 | values.append(end) |
paul@1230 | 952 | |
paul@1230 | 953 | query, values = self.get_query( |
paul@1230 | 954 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 955 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 956 | "table" : self.table_name |
paul@1230 | 957 | }, |
paul@1230 | 958 | columns, values) |
paul@1230 | 959 | |
paul@1230 | 960 | self.cursor.execute(query, values) |
paul@1230 | 961 | |
paul@1230 | 962 | return map(lambda t: self.make_period(t), self.cursor.fetchall()) |
paul@1230 | 963 | |
paul@1230 | 964 | def get_overlapping(self, periods): |
paul@1230 | 965 | |
paul@1230 | 966 | """ |
paul@1230 | 967 | Return the entries in the collection providing periods overlapping with |
paul@1230 | 968 | the given sorted collection of 'periods'. |
paul@1230 | 969 | """ |
paul@1230 | 970 | |
paul@1230 | 971 | overlapping = set() |
paul@1230 | 972 | |
paul@1230 | 973 | for period in periods: |
paul@1230 | 974 | columns, values = self._get_period_values(period) |
paul@1230 | 975 | |
paul@1230 | 976 | query, values = self.get_query( |
paul@1230 | 977 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 978 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 979 | "table" : self.table_name |
paul@1230 | 980 | }, |
paul@1230 | 981 | columns, values) |
paul@1230 | 982 | |
paul@1230 | 983 | self.cursor.execute(query, values) |
paul@1230 | 984 | |
paul@1230 | 985 | overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall())) |
paul@1230 | 986 | |
paul@1230 | 987 | overlapping = list(overlapping) |
paul@1230 | 988 | overlapping.sort() |
paul@1230 | 989 | return overlapping |
paul@1230 | 990 | |
paul@1230 | 991 | def remove_overlapping(self, period): |
paul@1230 | 992 | |
paul@1230 | 993 | "Remove all periods overlapping with 'period' from the collection." |
paul@1230 | 994 | |
paul@1230 | 995 | self._check_mutable() |
paul@1230 | 996 | |
paul@1230 | 997 | columns, values = self._get_period_values(period) |
paul@1230 | 998 | |
paul@1230 | 999 | query, values = self.get_query( |
paul@1230 | 1000 | "delete from %(table)s :condition" % { |
paul@1230 | 1001 | "table" : self.table_name |
paul@1230 | 1002 | }, |
paul@1230 | 1003 | columns, values) |
paul@1230 | 1004 | |
paul@1230 | 1005 | self.cursor.execute(query, values) |
paul@1230 | 1006 | |
paul@1230 | 1007 | def _get_period_values(self, period): |
paul@1230 | 1008 | |
paul@1230 | 1009 | start = format_datetime(period.get_start_point()) |
paul@1230 | 1010 | end = format_datetime(period.get_end_point()) |
paul@1230 | 1011 | |
paul@1230 | 1012 | columns, values = [], [] |
paul@1230 | 1013 | |
paul@1230 | 1014 | if end: |
paul@1230 | 1015 | columns.append("start < ?") |
paul@1230 | 1016 | values.append(end) |
paul@1230 | 1017 | if start: |
paul@1230 | 1018 | columns.append("end > ?") |
paul@1230 | 1019 | values.append(start) |
paul@1230 | 1020 | |
paul@1230 | 1021 | return columns, values |
paul@1230 | 1022 | |
paul@1230 | 1023 | class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection): |
paul@1230 | 1024 | |
paul@1230 | 1025 | "A collection of quota group free/busy objects." |
paul@1230 | 1026 | |
paul@1230 | 1027 | def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): |
paul@1230 | 1028 | |
paul@1230 | 1029 | """ |
paul@1230 | 1030 | Remove from the collection all periods associated with 'uid' and |
paul@1230 | 1031 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1230 | 1032 | be referenced) and any 'attendee'. |
paul@1230 | 1033 | |
paul@1230 | 1034 | Return the removed periods. |
paul@1230 | 1035 | """ |
paul@1230 | 1036 | |
paul@1230 | 1037 | self._check_mutable() |
paul@1230 | 1038 | |
paul@1230 | 1039 | columns, values = ["object_uid"], [uid] |
paul@1230 | 1040 | |
paul@1230 | 1041 | if recurrenceid: |
paul@1230 | 1042 | columns.append("object_recurrenceid") |
paul@1230 | 1043 | values.append(recurrenceid) |
paul@1230 | 1044 | else: |
paul@1230 | 1045 | columns.append("object_recurrenceid is null") |
paul@1230 | 1046 | |
paul@1230 | 1047 | if attendee: |
paul@1230 | 1048 | columns.append("attendee") |
paul@1230 | 1049 | values.append(attendee) |
paul@1230 | 1050 | else: |
paul@1230 | 1051 | columns.append("attendee is null") |
paul@1230 | 1052 | |
paul@1230 | 1053 | query, _values = self.get_query( |
paul@1230 | 1054 | "select %(columns)s from %(table)s :condition" % { |
paul@1230 | 1055 | "columns" : self.columnlist(self.period_columns), |
paul@1230 | 1056 | "table" : self.table_name |
paul@1230 | 1057 | }, |
paul@1230 | 1058 | columns, values) |
paul@1230 | 1059 | |
paul@1230 | 1060 | self.cursor.execute(query, _values) |
paul@1230 | 1061 | removed = self.cursor.fetchall() |
paul@1230 | 1062 | |
paul@1230 | 1063 | query, values = self.get_query( |
paul@1230 | 1064 | "delete from %(table)s :condition" % { |
paul@1230 | 1065 | "table" : self.table_name |
paul@1230 | 1066 | }, |
paul@1230 | 1067 | columns, values) |
paul@1230 | 1068 | |
paul@1230 | 1069 | self.cursor.execute(query, values) |
paul@1230 | 1070 | |
paul@1230 | 1071 | return map(lambda t: self.make_period(t), removed) |
paul@1230 | 1072 | |
paul@1230 | 1073 | class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection): |
paul@1230 | 1074 | |
paul@1230 | 1075 | "A collection of offered free/busy objects." |
paul@1230 | 1076 | |
paul@1230 | 1077 | pass |
paul@1230 | 1078 | |
paul@1230 | 1079 | # vim: tabstop=4 expandtab shiftwidth=4 |