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