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