1 #!/usr/bin/env python 2 3 """ 4 Common calendar client utilities. 5 6 Copyright (C) 2014, 2015 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 datetime import datetime, timedelta 23 from imiptools import config 24 from imiptools.data import Object, get_address, get_uri, get_window_end, \ 25 is_new_object, make_freebusy, to_part, \ 26 uri_dict, uri_items, uri_values 27 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \ 28 get_duration, get_timestamp 29 from imiptools.period import can_schedule, remove_period, \ 30 remove_additional_periods, remove_affected_period, \ 31 update_freebusy 32 from imiptools.profile import Preferences 33 import imip_store 34 35 class Client: 36 37 "Common handler and manager methods." 38 39 default_window_size = 100 40 organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST" 41 42 def __init__(self, user, messenger=None, store=None, publisher=None, preferences_dir=None): 43 44 """ 45 Initialise a calendar client with the current 'user', plus any 46 'messenger', 'store' and 'publisher' objects, indicating any specific 47 'preferences_dir'. 48 """ 49 50 self.user = user 51 self.messenger = messenger 52 self.store = store or imip_store.FileStore() 53 54 try: 55 self.publisher = publisher or imip_store.FilePublisher() 56 except OSError: 57 self.publisher = None 58 59 self.preferences_dir = preferences_dir 60 self.preferences = None 61 62 # Store-related methods. 63 64 def acquire_lock(self): 65 self.store.acquire_lock(self.user) 66 67 def release_lock(self): 68 self.store.release_lock(self.user) 69 70 # Preferences-related methods. 71 72 def get_preferences(self): 73 if not self.preferences and self.user: 74 self.preferences = Preferences(self.user, self.preferences_dir) 75 return self.preferences 76 77 def get_user_attributes(self): 78 prefs = self.get_preferences() 79 return prefs and prefs.get_all(["CN"]) or {} 80 81 def get_tzid(self): 82 prefs = self.get_preferences() 83 return prefs and prefs.get("TZID") or get_default_timezone() 84 85 def get_window_size(self): 86 prefs = self.get_preferences() 87 try: 88 return prefs and int(prefs.get("window_size")) or self.default_window_size 89 except (TypeError, ValueError): 90 return self.default_window_size 91 92 def get_window_end(self): 93 return get_window_end(self.get_tzid(), self.get_window_size()) 94 95 def is_participating(self): 96 97 "Return participation in the calendar system." 98 99 prefs = self.get_preferences() 100 return prefs and prefs.get("participating", config.PARTICIPATING_DEFAULT) != "no" or False 101 102 def is_sharing(self): 103 104 "Return whether free/busy information is being generally shared." 105 106 prefs = self.get_preferences() 107 return prefs and prefs.get("freebusy_sharing", config.SHARING_DEFAULT) == "share" or False 108 109 def is_bundling(self): 110 111 "Return whether free/busy information is being bundled in messages." 112 113 prefs = self.get_preferences() 114 return prefs and prefs.get("freebusy_bundling", config.BUNDLING_DEFAULT) == "always" or False 115 116 def is_notifying(self): 117 118 "Return whether recipients are notified about free/busy payloads." 119 120 prefs = self.get_preferences() 121 return prefs and prefs.get("freebusy_messages", config.NOTIFYING_DEFAULT) == "notify" or False 122 123 def is_publishing(self): 124 125 "Return whether free/busy information is being published as Web resources." 126 127 prefs = self.get_preferences() 128 return prefs and prefs.get("freebusy_publishing", config.PUBLISHING_DEFAULT) == "publish" or False 129 130 def is_refreshing(self): 131 132 "Return whether a recipient supports requests to refresh event details." 133 134 prefs = self.get_preferences() 135 return prefs and prefs.get("event_refreshing", config.REFRESHING_DEFAULT) == "always" or False 136 137 def allow_add(self): 138 return self.get_add_method_response() in ("add", "refresh") 139 140 def get_add_method_response(self): 141 prefs = self.get_preferences() 142 return prefs and prefs.get("add_method_response", config.ADD_RESPONSE_DEFAULT) or "refresh" 143 144 def get_offer_period(self): 145 146 "Decode a specification in the iCalendar duration format." 147 148 prefs = self.get_preferences() 149 duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT) 150 151 # NOTE: Should probably report an error somehow if None. 152 153 return duration and get_duration(duration) or None 154 155 def get_organiser_replacement(self): 156 prefs = self.get_preferences() 157 return prefs and prefs.get("organiser_replacement", config.ORGANISER_REPLACEMENT_DEFAULT) or "attendee" 158 159 def have_manager(self): 160 return config.MANAGER_INTERFACE 161 162 def get_permitted_values(self): 163 164 """ 165 Decode a specification of one of the following forms... 166 167 <minute values> 168 <hour values>:<minute values> 169 <hour values>:<minute values>:<second values> 170 171 ...with each list of values being comma-separated. 172 """ 173 174 prefs = self.get_preferences() 175 permitted_values = prefs and prefs.get("permitted_times") 176 if permitted_values: 177 try: 178 l = [] 179 for component in permitted_values.split(":")[:3]: 180 if component: 181 l.append(map(int, component.split(","))) 182 else: 183 l.append(None) 184 185 # NOTE: Should probably report an error somehow. 186 187 except ValueError: 188 return None 189 else: 190 l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or []) 191 return l 192 else: 193 return None 194 195 # Common operations on calendar data. 196 197 def update_attendees(self, obj, attendees, removed): 198 199 """ 200 Update the attendees in 'obj' with the given 'attendees' and 'removed' 201 attendee lists. A list is returned containing the attendees whose 202 attendance should be cancelled. 203 """ 204 205 to_cancel = [] 206 207 existing_attendees = uri_values(obj.get_values("ATTENDEE") or []) 208 added = set(attendees).difference(existing_attendees) 209 210 if added or removed: 211 attendees = uri_items(obj.get_items("ATTENDEE") or []) 212 sequence = obj.get_value("SEQUENCE") 213 214 if removed: 215 remaining = [] 216 217 for attendee, attendee_attr in attendees: 218 if attendee in removed: 219 220 # Without a sequence number, assume that the event has not 221 # been published and that attendees can be silently removed. 222 223 if sequence is not None: 224 to_cancel.append((attendee, attendee_attr)) 225 else: 226 remaining.append((attendee, attendee_attr)) 227 228 attendees = remaining 229 230 if added: 231 for attendee in added: 232 attendee = attendee.strip() 233 if attendee: 234 attendees.append((get_uri(attendee), {"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})) 235 236 obj["ATTENDEE"] = attendees 237 238 return to_cancel 239 240 def update_participation(self, obj, partstat=None): 241 242 """ 243 Update the participation in 'obj' of the user with the given 'partstat'. 244 """ 245 246 attendee_attr = uri_dict(obj.get_value_map("ATTENDEE")).get(self.user) 247 if not attendee_attr: 248 return None 249 if partstat: 250 attendee_attr["PARTSTAT"] = partstat 251 if attendee_attr.has_key("RSVP"): 252 del attendee_attr["RSVP"] 253 self.update_sender(attendee_attr) 254 return attendee_attr 255 256 def update_sender(self, attr): 257 258 "Update the SENT-BY attribute of the 'attr' sender metadata." 259 260 if self.messenger and self.messenger.sender != get_address(self.user): 261 attr["SENT-BY"] = get_uri(self.messenger.sender) 262 263 def get_periods(self, obj): 264 265 """ 266 Return periods for the given 'obj'. Interpretation of periods can depend 267 on the time zone, which is obtained for the current user. 268 """ 269 270 return obj.get_periods(self.get_tzid(), self.get_window_end()) 271 272 # Store operations. 273 274 def get_stored_object(self, uid, recurrenceid, section=None, username=None): 275 276 """ 277 Return the stored object for the current user, with the given 'uid' and 278 'recurrenceid' from the given 'section' and for the given 'username' (if 279 specified), or from the standard object collection otherwise. 280 """ 281 282 if section == "counters": 283 fragment = self.store.get_counter(self.user, username, uid, recurrenceid) 284 else: 285 fragment = self.store.get_event(self.user, uid, recurrenceid) 286 return fragment and Object(fragment) 287 288 # Free/busy operations. 289 290 def get_freebusy_part(self, freebusy=None): 291 292 """ 293 Return a message part containing free/busy information for the user, 294 either specified as 'freebusy' or obtained from the store directly. 295 """ 296 297 if self.is_sharing() and self.is_bundling(): 298 299 # Invent a unique identifier. 300 301 utcnow = get_timestamp() 302 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) 303 304 freebusy = freebusy or self.store.get_freebusy(self.user) 305 306 user_attr = {} 307 self.update_sender(user_attr) 308 return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)]) 309 310 return None 311 312 def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None): 313 314 """ 315 Update the 'freebusy' collection with the given 'periods', indicating a 316 'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a 317 recurrence or the parent event. The 'summary' and 'organiser' must also 318 be provided. 319 320 An optional 'expires' datetime string can be provided to tag a free/busy 321 offer. 322 """ 323 324 update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires) 325 326 class ClientForObject(Client): 327 328 "A client maintaining a specific object." 329 330 def __init__(self, obj, user, messenger=None, store=None, publisher=None, preferences_dir=None): 331 Client.__init__(self, user, messenger, store, publisher, preferences_dir) 332 self.set_object(obj) 333 334 def set_object(self, obj): 335 336 "Set the current object to 'obj', obtaining metadata details." 337 338 self.obj = obj 339 self.uid = obj and self.obj.get_uid() 340 self.recurrenceid = obj and self.obj.get_recurrenceid() 341 self.sequence = obj and self.obj.get_value("SEQUENCE") 342 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 343 344 def set_identity(self, method): 345 346 """ 347 Set the current user for the current object in the context of the given 348 'method'. It is usually set when initialising the handler, using the 349 recipient details, but outgoing messages do not reference the recipient 350 in this way. 351 """ 352 353 pass 354 355 def is_usable(self, method=None): 356 357 "Return whether the current object is usable with the given 'method'." 358 359 return True 360 361 # Object update methods. 362 363 def update_recurrenceid(self): 364 365 """ 366 Update the RECURRENCE-ID in the current object, initialising it from 367 DTSTART. 368 """ 369 370 self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")] 371 self.recurrenceid = self.obj.get_recurrenceid() 372 373 def update_dtstamp(self, obj=None): 374 375 "Update the DTSTAMP in the current object or any given object 'obj'." 376 377 obj = obj or self.obj 378 self.dtstamp = obj.update_dtstamp() 379 380 def update_sequence(self, increment=False, obj=None): 381 382 "Update the SEQUENCE in the current object or any given object 'obj'." 383 384 obj = obj or self.obj 385 obj.update_sequence(increment) 386 387 def merge_attendance(self, attendees): 388 389 """ 390 Merge attendance from the current object's 'attendees' into the version 391 stored for the current user. 392 """ 393 394 obj = self.get_stored_object_version() 395 396 if not obj or not self.have_new_object(): 397 return False 398 399 # Get attendee details in a usable form. 400 401 attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) 402 403 for attendee, attendee_attr in attendees.items(): 404 405 # Update attendance in the loaded object. 406 407 attendee_map[attendee] = attendee_attr 408 409 # Set the new details and store the object. 410 411 obj["ATTENDEE"] = attendee_map.items() 412 413 # Set a specific recurrence or the complete event if not an additional 414 # occurrence. 415 416 self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node()) 417 418 return True 419 420 # Object-related tests. 421 422 def is_recognised_organiser(self, organiser): 423 424 """ 425 Return whether the given 'organiser' is recognised from 426 previously-received details. If no stored details exist, True is 427 returned. 428 """ 429 430 obj = self.get_stored_object_version() 431 if obj: 432 stored_organiser = get_uri(obj.get_value("ORGANIZER")) 433 return stored_organiser == organiser 434 else: 435 return True 436 437 def is_recognised_attendee(self, attendee): 438 439 """ 440 Return whether the given 'attendee' is recognised from 441 previously-received details. If no stored details exist, True is 442 returned. 443 """ 444 445 obj = self.get_stored_object_version() 446 if obj: 447 stored_attendees = uri_dict(obj.get_value_map("ATTENDEE")) 448 return stored_attendees.has_key(attendee) 449 else: 450 return True 451 452 def get_attendance(self, user=None, obj=None): 453 454 """ 455 Return the attendance attributes for 'user', or the current user if 456 'user' is not specified. 457 """ 458 459 attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE")) 460 return attendees.get(user or self.user) 461 462 def is_participating(self, user, as_organiser=False, obj=None): 463 464 """ 465 Return whether, subject to the 'user' indicating an identity and the 466 'as_organiser' status of that identity, the user concerned is actually 467 participating in the current object event. 468 """ 469 470 # Use any attendee property information for an organiser, not the 471 # organiser property attributes. 472 473 attr = self.get_attendance(user, obj=obj) 474 return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") != "DECLINED" 475 476 def get_overriding_transparency(self, user, as_organiser=False): 477 478 """ 479 Return the overriding transparency to be associated with the free/busy 480 records for an event, subject to the 'user' indicating an identity and 481 the 'as_organiser' status of that identity. 482 483 Where an identity is only an organiser and not attending, "ORG" is 484 returned. Otherwise, no overriding transparency is defined and None is 485 returned. 486 """ 487 488 attr = self.get_attendance(user) 489 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 490 491 def can_schedule(self, freebusy, periods): 492 493 """ 494 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 495 """ 496 497 return can_schedule(freebusy, periods, self.uid, self.recurrenceid) 498 499 def have_new_object(self, strict=True): 500 501 """ 502 Return whether the current object is new to the current user. 503 504 If 'strict' is specified and is a false value, the DTSTAMP test will be 505 ignored. This is useful in handling responses from attendees from 506 clients (like Claws Mail) that erase time information from DTSTAMP and 507 make it invalid. 508 """ 509 510 obj = self.get_stored_object_version() 511 512 # If found, compare SEQUENCE and potentially DTSTAMP. 513 514 if obj: 515 sequence = obj.get_value("SEQUENCE") 516 dtstamp = obj.get_value("DTSTAMP") 517 518 # If the request refers to an older version of the object, ignore 519 # it. 520 521 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 522 523 return True 524 525 def possibly_recurring_indefinitely(self): 526 527 "Return whether the object recurs indefinitely." 528 529 # Obtain the stored object to make sure that recurrence information 530 # is not being ignored. This might happen if a client sends a 531 # cancellation without the complete set of properties, for instance. 532 533 return self.obj.possibly_recurring_indefinitely() or \ 534 self.get_stored_object_version() and \ 535 self.get_stored_object_version().possibly_recurring_indefinitely() 536 537 # Constraint application on event periods. 538 539 def check_object(self): 540 541 "Check the object against any scheduling constraints." 542 543 permitted_values = self.get_permitted_values() 544 if not permitted_values: 545 return None 546 547 invalid = [] 548 549 for period in self.obj.get_periods(self.get_tzid()): 550 start = period.get_start() 551 end = period.get_end() 552 start_errors = check_permitted_values(start, permitted_values) 553 end_errors = check_permitted_values(end, permitted_values) 554 if start_errors or end_errors: 555 invalid.append((period.origin, start_errors, end_errors)) 556 557 return invalid 558 559 def correct_object(self): 560 561 "Correct the object according to any scheduling constraints." 562 563 permitted_values = self.get_permitted_values() 564 return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) 565 566 # Object retrieval. 567 568 def get_stored_object_version(self): 569 570 """ 571 Return the stored object to which the current object refers for the 572 current user. 573 """ 574 575 return self.get_stored_object(self.uid, self.recurrenceid) 576 577 def get_definitive_object(self, as_organiser): 578 579 """ 580 Return an object considered definitive for the current transaction, 581 using 'as_organiser' to select the current transaction's object if 582 false, or selecting a stored object if true. 583 """ 584 585 return not as_organiser and self.obj or self.get_stored_object_version() 586 587 def get_parent_object(self): 588 589 """ 590 Return the parent object to which the current object refers for the 591 current user. 592 """ 593 594 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 595 596 # Convenience methods for modifying free/busy collections. 597 598 def get_recurrence_start_point(self, recurrenceid): 599 600 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 601 602 return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) 603 604 def remove_from_freebusy(self, freebusy): 605 606 "Remove this event from the given 'freebusy' collection." 607 608 if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid: 609 remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid)) 610 611 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 612 613 """ 614 Remove from 'freebusy' any original recurrence from parent free/busy 615 details for the current object, if the current object is a specific 616 additional recurrence. Otherwise, remove all additional recurrence 617 information corresponding to 'recurrenceids', or if omitted, all 618 recurrences. 619 """ 620 621 if self.recurrenceid: 622 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 623 remove_affected_period(freebusy, self.uid, recurrenceid) 624 else: 625 # Remove obsolete recurrence periods. 626 627 remove_additional_periods(freebusy, self.uid, recurrenceids) 628 629 # Remove original periods affected by additional recurrences. 630 631 if recurrenceids: 632 for recurrenceid in recurrenceids: 633 recurrenceid = self.get_recurrence_start_point(recurrenceid) 634 remove_affected_period(freebusy, self.uid, recurrenceid) 635 636 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 637 638 """ 639 Update the 'freebusy' collection for this event with the periods and 640 transparency associated with the current object, subject to the 'user' 641 identity and the attendance details provided for them, indicating 642 whether the update is being done 'as_organiser' (for the organiser of 643 an event) or not. 644 645 If 'offer' is set to a true value, any free/busy updates will be tagged 646 with an expiry time. 647 """ 648 649 # Obtain the stored object if the current object is not issued by the 650 # organiser. Attendees do not have the opportunity to redefine the 651 # periods. 652 653 obj = self.get_definitive_object(as_organiser) 654 if not obj: 655 return 656 657 # Obtain the affected periods. 658 659 periods = self.get_periods(obj) 660 661 # Define an overriding transparency, the indicated event transparency, 662 # or the default transparency for the free/busy entry. 663 664 transp = self.get_overriding_transparency(user, as_organiser) or \ 665 obj.get_value("TRANSP") or \ 666 "OPAQUE" 667 668 # Calculate any expiry time. If no offer period is defined, do not 669 # record the offer periods. 670 671 if offer: 672 offer_period = self.get_offer_period() 673 if offer_period: 674 expires = get_timestamp(offer_period) 675 else: 676 return 677 else: 678 expires = None 679 680 # Perform the low-level update. 681 682 Client.update_freebusy(self, freebusy, periods, transp, 683 self.uid, self.recurrenceid, 684 obj.get_value("SUMMARY"), 685 obj.get_value("ORGANIZER"), 686 expires) 687 688 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 689 updating_other=False, offer=False): 690 691 """ 692 Update the 'freebusy' collection for the given 'user', indicating 693 whether the update is 'for_organiser' (being done for the organiser of 694 an event) or not, and whether it is 'updating_other' (meaning another 695 user's details). 696 697 If 'offer' is set to a true value, any free/busy updates will be tagged 698 with an expiry time. 699 """ 700 701 # Record in the free/busy details unless a non-participating attendee. 702 # Remove periods for non-participating attendees. 703 704 if offer or self.is_participating(user, for_organiser and not updating_other): 705 self.update_freebusy(freebusy, user, 706 for_organiser and not updating_other or 707 not for_organiser and updating_other, 708 offer 709 ) 710 else: 711 self.remove_from_freebusy(freebusy) 712 713 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 714 updating_other=False): 715 716 """ 717 Remove details from the 'freebusy' collection for the given 'user', 718 indicating whether the modification is 'for_organiser' (being done for 719 the organiser of an event) or not, and whether it is 'updating_other' 720 (meaning another user's details). 721 """ 722 723 # Remove from the free/busy details if a specified attendee. 724 725 if self.is_participating(user, for_organiser and not updating_other): 726 self.remove_from_freebusy(freebusy) 727 728 # Convenience methods for updating stored free/busy information received 729 # from other users. 730 731 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 732 733 """ 734 For the current user, record the free/busy information for another 735 'user', indicating whether the update is 'for_organiser' or not, thus 736 maintaining a separate record of their free/busy details. 737 """ 738 739 fn = fn or self.update_freebusy_for_participant 740 741 # A user does not store free/busy information for themself as another 742 # party. 743 744 if user == self.user: 745 return 746 747 self.acquire_lock() 748 try: 749 freebusy = self.store.get_freebusy_for_other(self.user, user) 750 fn(freebusy, user, for_organiser, True) 751 752 # Tidy up any obsolete recurrences. 753 754 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 755 self.store.set_freebusy_for_other(self.user, freebusy, user) 756 757 finally: 758 self.release_lock() 759 760 def update_freebusy_from_organiser(self, organiser): 761 762 "For the current user, record free/busy information from 'organiser'." 763 764 self.update_freebusy_from_participant(organiser, True) 765 766 def update_freebusy_from_attendees(self, attendees): 767 768 "For the current user, record free/busy information from 'attendees'." 769 770 for attendee in attendees.keys(): 771 self.update_freebusy_from_participant(attendee, False) 772 773 def remove_freebusy_from_organiser(self, organiser): 774 775 "For the current user, remove free/busy information from 'organiser'." 776 777 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 778 779 def remove_freebusy_from_attendees(self, attendees): 780 781 "For the current user, remove free/busy information from 'attendees'." 782 783 for attendee in attendees.keys(): 784 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 785 786 # Convenience methods for updating free/busy details at the event level. 787 788 def update_event_in_freebusy(self, for_organiser=True): 789 790 """ 791 Update free/busy information when handling an object, doing so for the 792 organiser of an event if 'for_organiser' is set to a true value. 793 """ 794 795 freebusy = self.store.get_freebusy(self.user) 796 797 # Obtain the attendance attributes for this user, if available. 798 799 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 800 801 # Remove original recurrence details replaced by additional 802 # recurrences, as well as obsolete additional recurrences. 803 804 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 805 self.store.set_freebusy(self.user, freebusy) 806 807 if self.publisher and self.is_sharing() and self.is_publishing(): 808 self.publisher.set_freebusy(self.user, freebusy) 809 810 # Update free/busy provider information if the event may recur 811 # indefinitely. 812 813 if self.possibly_recurring_indefinitely(): 814 self.store.append_freebusy_provider(self.user, self.obj) 815 816 return True 817 818 def remove_event_from_freebusy(self): 819 820 "Remove free/busy information when handling an object." 821 822 freebusy = self.store.get_freebusy(self.user) 823 824 self.remove_from_freebusy(freebusy) 825 self.remove_freebusy_for_recurrences(freebusy) 826 self.store.set_freebusy(self.user, freebusy) 827 828 if self.publisher and self.is_sharing() and self.is_publishing(): 829 self.publisher.set_freebusy(self.user, freebusy) 830 831 # Update free/busy provider information if the event may recur 832 # indefinitely. 833 834 if self.possibly_recurring_indefinitely(): 835 self.store.remove_freebusy_provider(self.user, self.obj) 836 837 def update_event_in_freebusy_offers(self): 838 839 "Update free/busy offers when handling an object." 840 841 freebusy = self.store.get_freebusy_offers(self.user) 842 843 # Obtain the attendance attributes for this user, if available. 844 845 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 846 847 # Remove original recurrence details replaced by additional 848 # recurrences, as well as obsolete additional recurrences. 849 850 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 851 self.store.set_freebusy_offers(self.user, freebusy) 852 853 return True 854 855 def remove_event_from_freebusy_offers(self): 856 857 "Remove free/busy offers when handling an object." 858 859 freebusy = self.store.get_freebusy_offers(self.user) 860 861 self.remove_from_freebusy(freebusy) 862 self.remove_freebusy_for_recurrences(freebusy) 863 self.store.set_freebusy_offers(self.user, freebusy) 864 865 return True 866 867 # Convenience methods for removing counter-proposals and updating the 868 # request queue. 869 870 def remove_counter(self, attendee): 871 self.remove_counters([attendee]) 872 873 def remove_counters(self, attendees): 874 for attendee in attendees: 875 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 876 877 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 878 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 879 880 # vim: tabstop=4 expandtab shiftwidth=4