paul@1234 | 1 | #!/usr/bin/env python |
paul@1234 | 2 | |
paul@1234 | 3 | """ |
paul@1234 | 4 | Managing free/busy periods. |
paul@1234 | 5 | |
paul@1234 | 6 | Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk> |
paul@1234 | 7 | |
paul@1234 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@1234 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@1234 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@1234 | 11 | version. |
paul@1234 | 12 | |
paul@1234 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@1234 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@1234 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@1234 | 16 | details. |
paul@1234 | 17 | |
paul@1234 | 18 | You should have received a copy of the GNU General Public License along with |
paul@1234 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@1234 | 20 | """ |
paul@1234 | 21 | |
paul@1234 | 22 | from bisect import bisect_left, bisect_right |
paul@1234 | 23 | from imiptools.dates import format_datetime |
paul@1234 | 24 | from imiptools.period import get_overlapping, Period, PeriodBase |
paul@1234 | 25 | |
paul@1234 | 26 | # Conversion functions. |
paul@1234 | 27 | |
paul@1234 | 28 | def from_string(s, encoding): |
paul@1234 | 29 | |
paul@1234 | 30 | "Interpret 's' using 'encoding', preserving None." |
paul@1234 | 31 | |
paul@1234 | 32 | if s: |
paul@1234 | 33 | return unicode(s, encoding) |
paul@1234 | 34 | else: |
paul@1234 | 35 | return s |
paul@1234 | 36 | |
paul@1234 | 37 | def to_string(s, encoding): |
paul@1234 | 38 | |
paul@1234 | 39 | "Encode 's' using 'encoding', preserving None." |
paul@1234 | 40 | |
paul@1234 | 41 | if s: |
paul@1234 | 42 | return s.encode(encoding) |
paul@1234 | 43 | else: |
paul@1234 | 44 | return s |
paul@1234 | 45 | |
paul@1234 | 46 | |
paul@1234 | 47 | |
paul@1234 | 48 | # Period abstractions. |
paul@1234 | 49 | |
paul@1234 | 50 | class FreeBusyPeriod(PeriodBase): |
paul@1234 | 51 | |
paul@1234 | 52 | "A free/busy record abstraction." |
paul@1234 | 53 | |
paul@1234 | 54 | def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, |
paul@1234 | 55 | summary=None, organiser=None): |
paul@1234 | 56 | |
paul@1234 | 57 | """ |
paul@1234 | 58 | Initialise a free/busy period with the given 'start' and 'end' points, |
paul@1234 | 59 | plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' |
paul@1234 | 60 | details. |
paul@1234 | 61 | """ |
paul@1234 | 62 | |
paul@1234 | 63 | PeriodBase.__init__(self, start, end) |
paul@1234 | 64 | self.uid = uid |
paul@1234 | 65 | self.transp = transp or None |
paul@1234 | 66 | self.recurrenceid = recurrenceid or None |
paul@1234 | 67 | self.summary = summary or None |
paul@1234 | 68 | self.organiser = organiser or None |
paul@1234 | 69 | |
paul@1234 | 70 | def as_tuple(self, strings_only=False, string_datetimes=False): |
paul@1234 | 71 | |
paul@1234 | 72 | """ |
paul@1234 | 73 | Return the initialisation parameter tuple, converting datetimes and |
paul@1234 | 74 | false value parameters to strings if 'strings_only' is set to a true |
paul@1234 | 75 | value. Otherwise, if 'string_datetimes' is set to a true value, only the |
paul@1234 | 76 | datetime values are converted to strings. |
paul@1234 | 77 | """ |
paul@1234 | 78 | |
paul@1234 | 79 | null = lambda x: (strings_only and [""] or [x])[0] |
paul@1234 | 80 | return ( |
paul@1234 | 81 | (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start, |
paul@1234 | 82 | (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end, |
paul@1234 | 83 | self.uid or null(self.uid), |
paul@1234 | 84 | self.transp or strings_only and "OPAQUE" or None, |
paul@1234 | 85 | self.recurrenceid or null(self.recurrenceid), |
paul@1234 | 86 | self.summary or null(self.summary), |
paul@1234 | 87 | self.organiser or null(self.organiser) |
paul@1234 | 88 | ) |
paul@1234 | 89 | |
paul@1234 | 90 | def __cmp__(self, other): |
paul@1234 | 91 | |
paul@1234 | 92 | """ |
paul@1234 | 93 | Compare this object to 'other', employing the uid if the periods |
paul@1234 | 94 | involved are the same. |
paul@1234 | 95 | """ |
paul@1234 | 96 | |
paul@1234 | 97 | result = PeriodBase.__cmp__(self, other) |
paul@1234 | 98 | if result == 0 and isinstance(other, FreeBusyPeriod): |
paul@1234 | 99 | return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid)) |
paul@1234 | 100 | else: |
paul@1234 | 101 | return result |
paul@1234 | 102 | |
paul@1234 | 103 | def get_key(self): |
paul@1234 | 104 | return self.uid, self.recurrenceid, self.get_start() |
paul@1234 | 105 | |
paul@1234 | 106 | def __repr__(self): |
paul@1234 | 107 | return "FreeBusyPeriod%r" % (self.as_tuple(),) |
paul@1234 | 108 | |
paul@1234 | 109 | def get_tzid(self): |
paul@1234 | 110 | return "UTC" |
paul@1234 | 111 | |
paul@1234 | 112 | # Period and event recurrence logic. |
paul@1234 | 113 | |
paul@1234 | 114 | def is_replaced(self, recurrences): |
paul@1234 | 115 | |
paul@1234 | 116 | """ |
paul@1234 | 117 | Return whether this period refers to one of the 'recurrences'. |
paul@1234 | 118 | The 'recurrences' must be UTC datetimes corresponding to the start of |
paul@1234 | 119 | the period described by a recurrence. |
paul@1234 | 120 | """ |
paul@1234 | 121 | |
paul@1234 | 122 | for recurrence in recurrences: |
paul@1234 | 123 | if self.is_affected(recurrence): |
paul@1234 | 124 | return True |
paul@1234 | 125 | return False |
paul@1234 | 126 | |
paul@1234 | 127 | def is_affected(self, recurrence): |
paul@1234 | 128 | |
paul@1234 | 129 | """ |
paul@1234 | 130 | Return whether this period refers to 'recurrence'. The 'recurrence' must |
paul@1234 | 131 | be a UTC datetime corresponding to the start of the period described by |
paul@1234 | 132 | a recurrence. |
paul@1234 | 133 | """ |
paul@1234 | 134 | |
paul@1234 | 135 | return recurrence and self.get_start_point() == recurrence |
paul@1234 | 136 | |
paul@1234 | 137 | # Value correction methods. |
paul@1234 | 138 | |
paul@1234 | 139 | def make_corrected(self, start, end): |
paul@1234 | 140 | return self.__class__(start, end) |
paul@1234 | 141 | |
paul@1234 | 142 | class FreeBusyOfferPeriod(FreeBusyPeriod): |
paul@1234 | 143 | |
paul@1234 | 144 | "A free/busy record abstraction for an offer period." |
paul@1234 | 145 | |
paul@1234 | 146 | def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, |
paul@1234 | 147 | summary=None, organiser=None, expires=None): |
paul@1234 | 148 | |
paul@1234 | 149 | """ |
paul@1234 | 150 | Initialise a free/busy period with the given 'start' and 'end' points, |
paul@1234 | 151 | plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' |
paul@1234 | 152 | details. |
paul@1234 | 153 | |
paul@1234 | 154 | An additional 'expires' parameter can be used to indicate an expiry |
paul@1234 | 155 | datetime in conjunction with free/busy offers made when countering |
paul@1234 | 156 | event proposals. |
paul@1234 | 157 | """ |
paul@1234 | 158 | |
paul@1234 | 159 | FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, |
paul@1234 | 160 | summary, organiser) |
paul@1234 | 161 | self.expires = expires or None |
paul@1234 | 162 | |
paul@1234 | 163 | def as_tuple(self, strings_only=False, string_datetimes=False): |
paul@1234 | 164 | |
paul@1234 | 165 | """ |
paul@1234 | 166 | Return the initialisation parameter tuple, converting datetimes and |
paul@1234 | 167 | false value parameters to strings if 'strings_only' is set to a true |
paul@1234 | 168 | value. Otherwise, if 'string_datetimes' is set to a true value, only the |
paul@1234 | 169 | datetime values are converted to strings. |
paul@1234 | 170 | """ |
paul@1234 | 171 | |
paul@1234 | 172 | null = lambda x: (strings_only and [""] or [x])[0] |
paul@1234 | 173 | return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( |
paul@1234 | 174 | self.expires or null(self.expires),) |
paul@1234 | 175 | |
paul@1234 | 176 | def __repr__(self): |
paul@1234 | 177 | return "FreeBusyOfferPeriod%r" % (self.as_tuple(),) |
paul@1234 | 178 | |
paul@1234 | 179 | class FreeBusyGroupPeriod(FreeBusyPeriod): |
paul@1234 | 180 | |
paul@1234 | 181 | "A free/busy record abstraction for a quota group period." |
paul@1234 | 182 | |
paul@1234 | 183 | def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, |
paul@1234 | 184 | summary=None, organiser=None, attendee=None): |
paul@1234 | 185 | |
paul@1234 | 186 | """ |
paul@1234 | 187 | Initialise a free/busy period with the given 'start' and 'end' points, |
paul@1234 | 188 | plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' |
paul@1234 | 189 | details. |
paul@1234 | 190 | |
paul@1234 | 191 | An additional 'attendee' parameter can be used to indicate the identity |
paul@1234 | 192 | of the attendee recording the period. |
paul@1234 | 193 | """ |
paul@1234 | 194 | |
paul@1234 | 195 | FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, |
paul@1234 | 196 | summary, organiser) |
paul@1234 | 197 | self.attendee = attendee or None |
paul@1234 | 198 | |
paul@1234 | 199 | def as_tuple(self, strings_only=False, string_datetimes=False): |
paul@1234 | 200 | |
paul@1234 | 201 | """ |
paul@1234 | 202 | Return the initialisation parameter tuple, converting datetimes and |
paul@1234 | 203 | false value parameters to strings if 'strings_only' is set to a true |
paul@1234 | 204 | value. Otherwise, if 'string_datetimes' is set to a true value, only the |
paul@1234 | 205 | datetime values are converted to strings. |
paul@1234 | 206 | """ |
paul@1234 | 207 | |
paul@1234 | 208 | null = lambda x: (strings_only and [""] or [x])[0] |
paul@1234 | 209 | return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( |
paul@1234 | 210 | self.attendee or null(self.attendee),) |
paul@1234 | 211 | |
paul@1234 | 212 | def __cmp__(self, other): |
paul@1234 | 213 | |
paul@1234 | 214 | """ |
paul@1234 | 215 | Compare this object to 'other', employing the uid if the periods |
paul@1234 | 216 | involved are the same. |
paul@1234 | 217 | """ |
paul@1234 | 218 | |
paul@1234 | 219 | result = FreeBusyPeriod.__cmp__(self, other) |
paul@1234 | 220 | if isinstance(other, FreeBusyGroupPeriod) and result == 0: |
paul@1234 | 221 | return cmp(self.attendee, other.attendee) |
paul@1234 | 222 | else: |
paul@1234 | 223 | return result |
paul@1234 | 224 | |
paul@1234 | 225 | def __repr__(self): |
paul@1234 | 226 | return "FreeBusyGroupPeriod%r" % (self.as_tuple(),) |
paul@1234 | 227 | |
paul@1234 | 228 | class FreeBusyCollectionBase: |
paul@1234 | 229 | |
paul@1234 | 230 | "Common operations on free/busy period collections." |
paul@1234 | 231 | |
paul@1234 | 232 | period_columns = [ |
paul@1234 | 233 | "start", "end", "object_uid", "transp", "object_recurrenceid", |
paul@1234 | 234 | "summary", "organiser" |
paul@1234 | 235 | ] |
paul@1234 | 236 | |
paul@1234 | 237 | period_class = FreeBusyPeriod |
paul@1234 | 238 | |
paul@1234 | 239 | def __init__(self, mutable=True): |
paul@1234 | 240 | self.mutable = mutable |
paul@1234 | 241 | |
paul@1234 | 242 | def _check_mutable(self): |
paul@1234 | 243 | if not self.mutable: |
paul@1234 | 244 | raise TypeError, "Cannot mutate this collection." |
paul@1234 | 245 | |
paul@1234 | 246 | def copy(self): |
paul@1234 | 247 | |
paul@1234 | 248 | "Make an independent mutable copy of the collection." |
paul@1234 | 249 | |
paul@1234 | 250 | return FreeBusyCollection(list(self), True) |
paul@1234 | 251 | |
paul@1234 | 252 | def make_period(self, t): |
paul@1234 | 253 | |
paul@1234 | 254 | """ |
paul@1234 | 255 | Make a period using the given tuple of arguments and the collection's |
paul@1234 | 256 | column details. |
paul@1234 | 257 | """ |
paul@1234 | 258 | |
paul@1234 | 259 | args = [] |
paul@1234 | 260 | for arg, column in zip(t, self.period_columns): |
paul@1234 | 261 | args.append(from_string(arg, "utf-8")) |
paul@1234 | 262 | return self.period_class(*args) |
paul@1234 | 263 | |
paul@1234 | 264 | def make_tuple(self, t): |
paul@1234 | 265 | |
paul@1234 | 266 | """ |
paul@1234 | 267 | Return a tuple from the given tuple 't' conforming to the collection's |
paul@1234 | 268 | column details. |
paul@1234 | 269 | """ |
paul@1234 | 270 | |
paul@1234 | 271 | args = [] |
paul@1234 | 272 | for arg, column in zip(t, self.period_columns): |
paul@1234 | 273 | args.append(arg) |
paul@1234 | 274 | return tuple(args) |
paul@1234 | 275 | |
paul@1234 | 276 | # List emulation methods. |
paul@1234 | 277 | |
paul@1234 | 278 | def __iadd__(self, periods): |
paul@1234 | 279 | self.insert_periods(periods) |
paul@1234 | 280 | return self |
paul@1234 | 281 | |
paul@1234 | 282 | def append(self, period): |
paul@1234 | 283 | self.insert_period(period) |
paul@1234 | 284 | |
paul@1234 | 285 | # Operations. |
paul@1234 | 286 | |
paul@1234 | 287 | def insert_periods(self, periods): |
paul@1234 | 288 | |
paul@1234 | 289 | "Insert the given 'periods' into the collection." |
paul@1234 | 290 | |
paul@1234 | 291 | for p in periods: |
paul@1234 | 292 | self.insert_period(p) |
paul@1234 | 293 | |
paul@1234 | 294 | def can_schedule(self, periods, uid, recurrenceid): |
paul@1234 | 295 | |
paul@1234 | 296 | """ |
paul@1234 | 297 | Return whether the collection can accommodate the given 'periods' |
paul@1234 | 298 | employing the specified 'uid' and 'recurrenceid'. |
paul@1234 | 299 | """ |
paul@1234 | 300 | |
paul@1234 | 301 | for conflict in self.have_conflict(periods, True): |
paul@1234 | 302 | if conflict.uid != uid or conflict.recurrenceid != recurrenceid: |
paul@1234 | 303 | return False |
paul@1234 | 304 | |
paul@1234 | 305 | return True |
paul@1234 | 306 | |
paul@1234 | 307 | def have_conflict(self, periods, get_conflicts=False): |
paul@1234 | 308 | |
paul@1234 | 309 | """ |
paul@1234 | 310 | Return whether any period in the collection overlaps with the given |
paul@1234 | 311 | 'periods', returning a collection of such overlapping periods if |
paul@1234 | 312 | 'get_conflicts' is set to a true value. |
paul@1234 | 313 | """ |
paul@1234 | 314 | |
paul@1234 | 315 | conflicts = set() |
paul@1234 | 316 | for p in periods: |
paul@1234 | 317 | overlapping = self.period_overlaps(p, get_conflicts) |
paul@1234 | 318 | if overlapping: |
paul@1234 | 319 | if get_conflicts: |
paul@1234 | 320 | conflicts.update(overlapping) |
paul@1234 | 321 | else: |
paul@1234 | 322 | return True |
paul@1234 | 323 | |
paul@1234 | 324 | if get_conflicts: |
paul@1234 | 325 | return conflicts |
paul@1234 | 326 | else: |
paul@1234 | 327 | return False |
paul@1234 | 328 | |
paul@1234 | 329 | def period_overlaps(self, period, get_periods=False): |
paul@1234 | 330 | |
paul@1234 | 331 | """ |
paul@1234 | 332 | Return whether any period in the collection overlaps with the given |
paul@1234 | 333 | 'period', returning a collection of overlapping periods if 'get_periods' |
paul@1234 | 334 | is set to a true value. |
paul@1234 | 335 | """ |
paul@1234 | 336 | |
paul@1234 | 337 | overlapping = self.get_overlapping([period]) |
paul@1234 | 338 | |
paul@1234 | 339 | if get_periods: |
paul@1234 | 340 | return overlapping |
paul@1234 | 341 | else: |
paul@1234 | 342 | return len(overlapping) != 0 |
paul@1234 | 343 | |
paul@1234 | 344 | def replace_overlapping(self, period, replacements): |
paul@1234 | 345 | |
paul@1234 | 346 | """ |
paul@1234 | 347 | Replace existing periods in the collection within the given 'period', |
paul@1234 | 348 | using the given 'replacements'. |
paul@1234 | 349 | """ |
paul@1234 | 350 | |
paul@1234 | 351 | self._check_mutable() |
paul@1234 | 352 | |
paul@1234 | 353 | self.remove_overlapping(period) |
paul@1234 | 354 | for replacement in replacements: |
paul@1234 | 355 | self.insert_period(replacement) |
paul@1234 | 356 | |
paul@1234 | 357 | def coalesce_freebusy(self): |
paul@1234 | 358 | |
paul@1234 | 359 | "Coalesce the periods in the collection, returning a new collection." |
paul@1234 | 360 | |
paul@1234 | 361 | if not self: |
paul@1234 | 362 | return FreeBusyCollection() |
paul@1234 | 363 | |
paul@1234 | 364 | fb = [] |
paul@1234 | 365 | |
paul@1234 | 366 | it = iter(self) |
paul@1234 | 367 | period = it.next() |
paul@1234 | 368 | |
paul@1234 | 369 | start = period.get_start_point() |
paul@1234 | 370 | end = period.get_end_point() |
paul@1234 | 371 | |
paul@1234 | 372 | try: |
paul@1234 | 373 | while True: |
paul@1234 | 374 | period = it.next() |
paul@1234 | 375 | if period.get_start_point() > end: |
paul@1234 | 376 | fb.append(self.period_class(start, end)) |
paul@1234 | 377 | start = period.get_start_point() |
paul@1234 | 378 | end = period.get_end_point() |
paul@1234 | 379 | else: |
paul@1234 | 380 | end = max(end, period.get_end_point()) |
paul@1234 | 381 | except StopIteration: |
paul@1234 | 382 | pass |
paul@1234 | 383 | |
paul@1234 | 384 | fb.append(self.period_class(start, end)) |
paul@1234 | 385 | return FreeBusyCollection(fb) |
paul@1234 | 386 | |
paul@1234 | 387 | def invert_freebusy(self): |
paul@1234 | 388 | |
paul@1234 | 389 | "Return the free periods from the collection as a new collection." |
paul@1234 | 390 | |
paul@1234 | 391 | if not self: |
paul@1234 | 392 | return FreeBusyCollection([self.period_class(None, None)]) |
paul@1234 | 393 | |
paul@1234 | 394 | # Coalesce periods that overlap or are adjacent. |
paul@1234 | 395 | |
paul@1234 | 396 | fb = self.coalesce_freebusy() |
paul@1234 | 397 | free = [] |
paul@1234 | 398 | |
paul@1234 | 399 | # Add a start-of-time period if appropriate. |
paul@1234 | 400 | |
paul@1234 | 401 | first = fb[0].get_start_point() |
paul@1234 | 402 | if first: |
paul@1234 | 403 | free.append(self.period_class(None, first)) |
paul@1234 | 404 | |
paul@1234 | 405 | start = fb[0].get_end_point() |
paul@1234 | 406 | |
paul@1234 | 407 | for period in fb[1:]: |
paul@1234 | 408 | free.append(self.period_class(start, period.get_start_point())) |
paul@1234 | 409 | start = period.get_end_point() |
paul@1234 | 410 | |
paul@1234 | 411 | # Add an end-of-time period if appropriate. |
paul@1234 | 412 | |
paul@1234 | 413 | if start: |
paul@1234 | 414 | free.append(self.period_class(start, None)) |
paul@1234 | 415 | |
paul@1234 | 416 | return FreeBusyCollection(free) |
paul@1234 | 417 | |
paul@1234 | 418 | def _update_freebusy(self, periods, uid, recurrenceid): |
paul@1234 | 419 | |
paul@1234 | 420 | """ |
paul@1234 | 421 | Update the free/busy details with the given 'periods', using the given |
paul@1234 | 422 | 'uid' plus 'recurrenceid' to remove existing periods. |
paul@1234 | 423 | """ |
paul@1234 | 424 | |
paul@1234 | 425 | self._check_mutable() |
paul@1234 | 426 | |
paul@1234 | 427 | self.remove_specific_event_periods(uid, recurrenceid) |
paul@1234 | 428 | |
paul@1234 | 429 | self.insert_periods(periods) |
paul@1234 | 430 | |
paul@1234 | 431 | def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser): |
paul@1234 | 432 | |
paul@1234 | 433 | """ |
paul@1234 | 434 | Update the free/busy details with the given 'periods', 'transp' setting, |
paul@1234 | 435 | 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. |
paul@1234 | 436 | """ |
paul@1234 | 437 | |
paul@1234 | 438 | new_periods = [] |
paul@1234 | 439 | |
paul@1234 | 440 | for p in periods: |
paul@1234 | 441 | new_periods.append( |
paul@1234 | 442 | self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser) |
paul@1234 | 443 | ) |
paul@1234 | 444 | |
paul@1234 | 445 | self._update_freebusy(new_periods, uid, recurrenceid) |
paul@1234 | 446 | |
paul@1234 | 447 | class SupportAttendee: |
paul@1234 | 448 | |
paul@1234 | 449 | "A mix-in that supports the affected attendee in free/busy periods." |
paul@1234 | 450 | |
paul@1234 | 451 | period_columns = FreeBusyCollectionBase.period_columns + ["attendee"] |
paul@1234 | 452 | period_class = FreeBusyGroupPeriod |
paul@1234 | 453 | |
paul@1234 | 454 | def _update_freebusy(self, periods, uid, recurrenceid, attendee=None): |
paul@1234 | 455 | |
paul@1234 | 456 | """ |
paul@1234 | 457 | Update the free/busy details with the given 'periods', using the given |
paul@1234 | 458 | 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods. |
paul@1234 | 459 | """ |
paul@1234 | 460 | |
paul@1234 | 461 | self._check_mutable() |
paul@1234 | 462 | |
paul@1234 | 463 | self.remove_specific_event_periods(uid, recurrenceid, attendee) |
paul@1234 | 464 | |
paul@1234 | 465 | self.insert_periods(periods) |
paul@1234 | 466 | |
paul@1234 | 467 | def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None): |
paul@1234 | 468 | |
paul@1234 | 469 | """ |
paul@1234 | 470 | Update the free/busy details with the given 'periods', 'transp' setting, |
paul@1234 | 471 | 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. |
paul@1234 | 472 | |
paul@1234 | 473 | An optional 'attendee' indicates the attendee affected by the period. |
paul@1234 | 474 | """ |
paul@1234 | 475 | |
paul@1234 | 476 | new_periods = [] |
paul@1234 | 477 | |
paul@1234 | 478 | for p in periods: |
paul@1234 | 479 | new_periods.append( |
paul@1234 | 480 | self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee) |
paul@1234 | 481 | ) |
paul@1234 | 482 | |
paul@1234 | 483 | self._update_freebusy(new_periods, uid, recurrenceid, attendee) |
paul@1234 | 484 | |
paul@1234 | 485 | class SupportExpires: |
paul@1234 | 486 | |
paul@1234 | 487 | "A mix-in that supports the expiry datetime in free/busy periods." |
paul@1234 | 488 | |
paul@1234 | 489 | period_columns = FreeBusyCollectionBase.period_columns + ["expires"] |
paul@1234 | 490 | period_class = FreeBusyOfferPeriod |
paul@1234 | 491 | |
paul@1234 | 492 | def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None): |
paul@1234 | 493 | |
paul@1234 | 494 | """ |
paul@1234 | 495 | Update the free/busy details with the given 'periods', 'transp' setting, |
paul@1234 | 496 | 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. |
paul@1234 | 497 | |
paul@1234 | 498 | An optional 'expires' datetime string indicates the expiry time of any |
paul@1234 | 499 | free/busy offer. |
paul@1234 | 500 | """ |
paul@1234 | 501 | |
paul@1234 | 502 | new_periods = [] |
paul@1234 | 503 | |
paul@1234 | 504 | for p in periods: |
paul@1234 | 505 | new_periods.append( |
paul@1234 | 506 | self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires) |
paul@1234 | 507 | ) |
paul@1234 | 508 | |
paul@1234 | 509 | self._update_freebusy(new_periods, uid, recurrenceid) |
paul@1234 | 510 | |
paul@1234 | 511 | |
paul@1234 | 512 | |
paul@1234 | 513 | # Simple abstractions suitable for use with file-based representations and as |
paul@1234 | 514 | # general copies of collections. |
paul@1234 | 515 | |
paul@1234 | 516 | class FreeBusyCollection(FreeBusyCollectionBase): |
paul@1234 | 517 | |
paul@1234 | 518 | "An abstraction for a collection of free/busy periods." |
paul@1234 | 519 | |
paul@1234 | 520 | def __init__(self, periods=None, mutable=True): |
paul@1234 | 521 | |
paul@1234 | 522 | """ |
paul@1234 | 523 | Initialise the collection with the given list of 'periods', or start an |
paul@1234 | 524 | empty collection if no list is given. If 'mutable' is indicated, the |
paul@1234 | 525 | collection may be changed; otherwise, an exception will be raised. |
paul@1234 | 526 | """ |
paul@1234 | 527 | |
paul@1234 | 528 | FreeBusyCollectionBase.__init__(self, mutable) |
paul@1234 | 529 | self.periods = periods or [] |
paul@1234 | 530 | |
paul@1234 | 531 | # List emulation methods. |
paul@1234 | 532 | |
paul@1234 | 533 | def __nonzero__(self): |
paul@1234 | 534 | return bool(self.periods) |
paul@1234 | 535 | |
paul@1234 | 536 | def __iter__(self): |
paul@1234 | 537 | return iter(self.periods) |
paul@1234 | 538 | |
paul@1234 | 539 | def __len__(self): |
paul@1234 | 540 | return len(self.periods) |
paul@1234 | 541 | |
paul@1234 | 542 | def __getitem__(self, i): |
paul@1234 | 543 | return self.periods[i] |
paul@1234 | 544 | |
paul@1234 | 545 | # Operations. |
paul@1234 | 546 | |
paul@1234 | 547 | def insert_period(self, period): |
paul@1234 | 548 | |
paul@1234 | 549 | "Insert the given 'period' into the collection." |
paul@1234 | 550 | |
paul@1234 | 551 | self._check_mutable() |
paul@1234 | 552 | |
paul@1234 | 553 | i = bisect_left(self.periods, period) |
paul@1234 | 554 | if i == len(self.periods): |
paul@1234 | 555 | self.periods.append(period) |
paul@1234 | 556 | elif self.periods[i] != period: |
paul@1234 | 557 | self.periods.insert(i, period) |
paul@1234 | 558 | |
paul@1234 | 559 | def remove_periods(self, periods): |
paul@1234 | 560 | |
paul@1234 | 561 | "Remove the given 'periods' from the collection." |
paul@1234 | 562 | |
paul@1234 | 563 | self._check_mutable() |
paul@1234 | 564 | |
paul@1234 | 565 | for period in periods: |
paul@1234 | 566 | i = bisect_left(self.periods, period) |
paul@1234 | 567 | if i < len(self.periods) and self.periods[i] == period: |
paul@1234 | 568 | del self.periods[i] |
paul@1234 | 569 | |
paul@1234 | 570 | def remove_event_periods(self, uid, recurrenceid=None, participant=None): |
paul@1234 | 571 | |
paul@1234 | 572 | """ |
paul@1234 | 573 | Remove from the collection all periods associated with 'uid' and |
paul@1234 | 574 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1234 | 575 | be referenced). |
paul@1234 | 576 | |
paul@1234 | 577 | If 'participant' is specified, only remove periods for which the |
paul@1234 | 578 | participant is given as attending. |
paul@1234 | 579 | |
paul@1234 | 580 | Return the removed periods. |
paul@1234 | 581 | """ |
paul@1234 | 582 | |
paul@1234 | 583 | self._check_mutable() |
paul@1234 | 584 | |
paul@1234 | 585 | removed = [] |
paul@1234 | 586 | i = 0 |
paul@1234 | 587 | while i < len(self.periods): |
paul@1234 | 588 | fb = self.periods[i] |
paul@1234 | 589 | |
paul@1234 | 590 | if fb.uid == uid and fb.recurrenceid == recurrenceid and \ |
paul@1234 | 591 | (not participant or participant == fb.attendee): |
paul@1234 | 592 | |
paul@1234 | 593 | removed.append(self.periods[i]) |
paul@1234 | 594 | del self.periods[i] |
paul@1234 | 595 | else: |
paul@1234 | 596 | i += 1 |
paul@1234 | 597 | |
paul@1234 | 598 | return removed |
paul@1234 | 599 | |
paul@1234 | 600 | # Specific period removal when updating event details. |
paul@1234 | 601 | |
paul@1234 | 602 | remove_specific_event_periods = remove_event_periods |
paul@1234 | 603 | |
paul@1234 | 604 | def remove_additional_periods(self, uid, recurrenceids=None): |
paul@1234 | 605 | |
paul@1234 | 606 | """ |
paul@1234 | 607 | Remove from the collection all periods associated with 'uid' having a |
paul@1234 | 608 | recurrence identifier indicating an additional or modified period. |
paul@1234 | 609 | |
paul@1234 | 610 | If 'recurrenceids' is specified, remove all periods associated with |
paul@1234 | 611 | 'uid' that do not have a recurrence identifier in the given list. |
paul@1234 | 612 | |
paul@1234 | 613 | Return the removed periods. |
paul@1234 | 614 | """ |
paul@1234 | 615 | |
paul@1234 | 616 | self._check_mutable() |
paul@1234 | 617 | |
paul@1234 | 618 | removed = [] |
paul@1234 | 619 | i = 0 |
paul@1234 | 620 | while i < len(self.periods): |
paul@1234 | 621 | fb = self.periods[i] |
paul@1234 | 622 | if fb.uid == uid and fb.recurrenceid and ( |
paul@1234 | 623 | recurrenceids is None or |
paul@1234 | 624 | recurrenceids is not None and fb.recurrenceid not in recurrenceids |
paul@1234 | 625 | ): |
paul@1234 | 626 | removed.append(self.periods[i]) |
paul@1234 | 627 | del self.periods[i] |
paul@1234 | 628 | else: |
paul@1234 | 629 | i += 1 |
paul@1234 | 630 | |
paul@1234 | 631 | return removed |
paul@1234 | 632 | |
paul@1234 | 633 | def remove_affected_period(self, uid, start, participant=None): |
paul@1234 | 634 | |
paul@1234 | 635 | """ |
paul@1234 | 636 | Remove from the collection the period associated with 'uid' that |
paul@1234 | 637 | provides an occurrence starting at the given 'start' (provided by a |
paul@1234 | 638 | recurrence identifier, converted to a datetime). A recurrence identifier |
paul@1234 | 639 | is used to provide an alternative time period whilst also acting as a |
paul@1234 | 640 | reference to the originally-defined occurrence. |
paul@1234 | 641 | |
paul@1234 | 642 | If 'participant' is specified, only remove periods for which the |
paul@1234 | 643 | participant is given as attending. |
paul@1234 | 644 | |
paul@1234 | 645 | Return any removed period in a list. |
paul@1234 | 646 | """ |
paul@1234 | 647 | |
paul@1234 | 648 | self._check_mutable() |
paul@1234 | 649 | |
paul@1234 | 650 | removed = [] |
paul@1234 | 651 | |
paul@1234 | 652 | search = Period(start, start) |
paul@1234 | 653 | found = bisect_left(self.periods, search) |
paul@1234 | 654 | |
paul@1234 | 655 | while found < len(self.periods): |
paul@1234 | 656 | fb = self.periods[found] |
paul@1234 | 657 | |
paul@1234 | 658 | # Stop looking if the start no longer matches the recurrence identifier. |
paul@1234 | 659 | |
paul@1234 | 660 | if fb.get_start_point() != search.get_start_point(): |
paul@1234 | 661 | break |
paul@1234 | 662 | |
paul@1234 | 663 | # If the period belongs to the parent object, remove it and return. |
paul@1234 | 664 | |
paul@1234 | 665 | if not fb.recurrenceid and uid == fb.uid and \ |
paul@1234 | 666 | (not participant or participant == fb.attendee): |
paul@1234 | 667 | |
paul@1234 | 668 | removed.append(self.periods[found]) |
paul@1234 | 669 | del self.periods[found] |
paul@1234 | 670 | break |
paul@1234 | 671 | |
paul@1234 | 672 | # Otherwise, keep looking for a matching period. |
paul@1234 | 673 | |
paul@1234 | 674 | found += 1 |
paul@1234 | 675 | |
paul@1234 | 676 | return removed |
paul@1234 | 677 | |
paul@1234 | 678 | def periods_from(self, period): |
paul@1234 | 679 | |
paul@1234 | 680 | "Return the entries in the collection at or after 'period'." |
paul@1234 | 681 | |
paul@1234 | 682 | first = bisect_left(self.periods, period) |
paul@1234 | 683 | return self.periods[first:] |
paul@1234 | 684 | |
paul@1234 | 685 | def periods_until(self, period): |
paul@1234 | 686 | |
paul@1234 | 687 | "Return the entries in the collection before 'period'." |
paul@1234 | 688 | |
paul@1234 | 689 | last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid())) |
paul@1234 | 690 | return self.periods[:last] |
paul@1234 | 691 | |
paul@1234 | 692 | def get_overlapping(self, periods): |
paul@1234 | 693 | |
paul@1234 | 694 | """ |
paul@1234 | 695 | Return the entries in the collection providing periods overlapping with |
paul@1234 | 696 | the given sorted collection of 'periods'. |
paul@1234 | 697 | """ |
paul@1234 | 698 | |
paul@1234 | 699 | return get_overlapping(self.periods, periods) |
paul@1234 | 700 | |
paul@1234 | 701 | def remove_overlapping(self, period): |
paul@1234 | 702 | |
paul@1234 | 703 | "Remove all periods overlapping with 'period' from the collection." |
paul@1234 | 704 | |
paul@1234 | 705 | self._check_mutable() |
paul@1234 | 706 | |
paul@1234 | 707 | overlapping = self.get_overlapping([period]) |
paul@1234 | 708 | |
paul@1234 | 709 | if overlapping: |
paul@1234 | 710 | for fb in overlapping: |
paul@1234 | 711 | self.periods.remove(fb) |
paul@1234 | 712 | |
paul@1234 | 713 | class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection): |
paul@1234 | 714 | |
paul@1234 | 715 | "A collection of quota group free/busy objects." |
paul@1234 | 716 | |
paul@1234 | 717 | def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): |
paul@1234 | 718 | |
paul@1234 | 719 | """ |
paul@1234 | 720 | Remove from the collection all periods associated with 'uid' and |
paul@1234 | 721 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1234 | 722 | be referenced) and any 'attendee'. |
paul@1234 | 723 | |
paul@1234 | 724 | Return the removed periods. |
paul@1234 | 725 | """ |
paul@1234 | 726 | |
paul@1234 | 727 | self._check_mutable() |
paul@1234 | 728 | |
paul@1234 | 729 | removed = [] |
paul@1234 | 730 | i = 0 |
paul@1234 | 731 | while i < len(self.periods): |
paul@1234 | 732 | fb = self.periods[i] |
paul@1234 | 733 | if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee: |
paul@1234 | 734 | removed.append(self.periods[i]) |
paul@1234 | 735 | del self.periods[i] |
paul@1234 | 736 | else: |
paul@1234 | 737 | i += 1 |
paul@1234 | 738 | |
paul@1234 | 739 | return removed |
paul@1234 | 740 | |
paul@1234 | 741 | class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection): |
paul@1234 | 742 | |
paul@1234 | 743 | "A collection of offered free/busy objects." |
paul@1234 | 744 | |
paul@1234 | 745 | pass |
paul@1234 | 746 | |
paul@1234 | 747 | # vim: tabstop=4 expandtab shiftwidth=4 |