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_parts, 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_sender(self, attr): 198 199 "Update the SENT-BY attribute of the 'attr' sender metadata." 200 201 if self.messenger and self.messenger.sender != get_address(self.user): 202 attr["SENT-BY"] = get_uri(self.messenger.sender) 203 204 def get_periods(self, obj): 205 206 """ 207 Return periods for the given 'obj'. Interpretation of periods can depend 208 on the time zone, which is obtained for the current user. 209 """ 210 211 return obj.get_periods(self.get_tzid(), self.get_window_end()) 212 213 # Store operations. 214 215 def get_stored_object(self, uid, recurrenceid, section=None, username=None): 216 217 """ 218 Return the stored object for the current user, with the given 'uid' and 219 'recurrenceid' from the given 'section' and for the given 'username' (if 220 specified), or from the standard object collection otherwise. 221 """ 222 223 if section == "counters": 224 fragment = self.store.get_counter(self.user, username, uid, recurrenceid) 225 else: 226 fragment = self.store.get_event(self.user, uid, recurrenceid) 227 return fragment and Object(fragment) 228 229 # Free/busy operations. 230 231 def get_freebusy_part(self, freebusy=None): 232 233 """ 234 Return a message part containing free/busy information for the user, 235 either specified as 'freebusy' or obtained from the store directly. 236 """ 237 238 if self.is_sharing() and self.is_bundling(): 239 240 # Invent a unique identifier. 241 242 utcnow = get_timestamp() 243 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) 244 245 freebusy = freebusy or self.store.get_freebusy(self.user) 246 247 user_attr = {} 248 self.update_sender(user_attr) 249 return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)]) 250 251 return None 252 253 def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None): 254 255 """ 256 Update the 'freebusy' collection with the given 'periods', indicating a 257 'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a 258 recurrence or the parent event. The 'summary' and 'organiser' must also 259 be provided. 260 261 An optional 'expires' datetime string can be provided to tag a free/busy 262 offer. 263 """ 264 265 update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires) 266 267 class ClientForObject(Client): 268 269 "A client maintaining a specific object." 270 271 def __init__(self, obj, user, messenger=None, store=None, publisher=None, preferences_dir=None): 272 Client.__init__(self, user, messenger, store, publisher, preferences_dir) 273 self.set_object(obj) 274 275 def set_object(self, obj): 276 277 "Set the current object to 'obj', obtaining metadata details." 278 279 self.obj = obj 280 self.uid = obj and self.obj.get_uid() 281 self.recurrenceid = obj and self.obj.get_recurrenceid() 282 self.sequence = obj and self.obj.get_value("SEQUENCE") 283 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 284 285 def is_usable(self, method=None): 286 287 "Return whether the current object is usable with the given 'method'." 288 289 return True 290 291 def is_organiser(self): 292 293 """ 294 Return whether the current user is the organiser in the current object. 295 """ 296 297 return get_uri(self.obj.get_value("ORGANIZER")) == self.user 298 299 # Object update methods. 300 301 def update_recurrenceid(self): 302 303 """ 304 Update the RECURRENCE-ID in the current object, initialising it from 305 DTSTART. 306 """ 307 308 self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")] 309 self.recurrenceid = self.obj.get_recurrenceid() 310 311 def update_dtstamp(self, obj=None): 312 313 "Update the DTSTAMP in the current object or any given object 'obj'." 314 315 obj = obj or self.obj 316 self.dtstamp = obj.update_dtstamp() 317 318 def update_sequence(self, increment=False, obj=None): 319 320 "Update the SEQUENCE in the current object or any given object 'obj'." 321 322 obj = obj or self.obj 323 obj.update_sequence(increment) 324 325 def merge_attendance(self, attendees): 326 327 """ 328 Merge attendance from the current object's 'attendees' into the version 329 stored for the current user. 330 """ 331 332 obj = self.get_stored_object_version() 333 334 if not obj or not self.have_new_object(): 335 return False 336 337 # Get attendee details in a usable form. 338 339 attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) 340 341 for attendee, attendee_attr in attendees.items(): 342 343 # Update attendance in the loaded object. 344 345 attendee_map[attendee] = attendee_attr 346 347 # Set the new details and store the object. 348 349 obj["ATTENDEE"] = attendee_map.items() 350 351 # Set a specific recurrence or the complete event if not an additional 352 # occurrence. 353 354 self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node()) 355 356 return True 357 358 def update_attendees(self, attendees, removed): 359 360 """ 361 Update the attendees in the current object with the given 'attendees' 362 and 'removed' attendee lists. 363 364 A tuple is returned containing two items: a list of the attendees whose 365 attendance is being proposed (in a counter-proposal), a list of the 366 attendees whose attendance should be cancelled. 367 """ 368 369 to_cancel = [] 370 371 existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or []) 372 existing_attendees_map = dict(existing_attendees) 373 374 # Added attendees are those from the supplied collection not already 375 # present in the object. 376 377 added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees]) 378 379 # NOTE: When countering, no removals will occur, but additions might. 380 381 if added or removed: 382 383 # The organiser can remove existing attendees. 384 385 if removed and self.is_organiser(): 386 remaining = [] 387 388 for attendee, attendee_attr in existing_attendees: 389 if attendee in removed: 390 391 # Only when an event has not been published can 392 # attendees be silently removed. 393 394 if obj.is_shared(): 395 to_cancel.append((attendee, attendee_attr)) 396 else: 397 remaining.append((attendee, attendee_attr)) 398 399 existing_attendees = remaining 400 401 # Attendees (when countering) must only include the current user and 402 # any added attendees. 403 404 elif not self.is_organiser(): 405 existing_attendees = [] 406 407 # Both organisers and attendees (when countering) can add attendees. 408 409 if added: 410 411 # Obtain a mapping from URIs to name details. 412 413 attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)]) 414 415 for attendee in added: 416 attendee = attendee.strip() 417 if attendee: 418 cn = attendee_map.get(attendee) 419 attendee_attr = {"CN" : cn} or {} 420 421 # Only the organiser can reset the participation attributes. 422 423 if self.is_organiser(): 424 attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}) 425 426 existing_attendees.append((attendee, attendee_attr)) 427 428 # Attendees (when countering) must only include the current user and 429 # any added attendees. 430 431 if not self.is_organiser() and self.user not in existing_attendees: 432 user_attr = self.get_user_attributes() 433 user_attr.update(existing_attendees_map.get(self.user) or {}) 434 existing_attendees.append((self.user, user_attr)) 435 436 self.obj["ATTENDEE"] = existing_attendees 437 438 return added, to_cancel 439 440 def update_participation(self, partstat=None): 441 442 """ 443 Update the participation in the current object of the user with the 444 given 'partstat'. 445 """ 446 447 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user) 448 if not attendee_attr: 449 return None 450 if partstat: 451 attendee_attr["PARTSTAT"] = partstat 452 if attendee_attr.has_key("RSVP"): 453 del attendee_attr["RSVP"] 454 self.update_sender(attendee_attr) 455 return attendee_attr 456 457 # Object-related tests. 458 459 def is_recognised_organiser(self, organiser): 460 461 """ 462 Return whether the given 'organiser' is recognised from 463 previously-received details. If no stored details exist, True is 464 returned. 465 """ 466 467 obj = self.get_stored_object_version() 468 if obj: 469 stored_organiser = get_uri(obj.get_value("ORGANIZER")) 470 return stored_organiser == organiser 471 else: 472 return True 473 474 def is_recognised_attendee(self, attendee): 475 476 """ 477 Return whether the given 'attendee' is recognised from 478 previously-received details. If no stored details exist, True is 479 returned. 480 """ 481 482 obj = self.get_stored_object_version() 483 if obj: 484 stored_attendees = uri_dict(obj.get_value_map("ATTENDEE")) 485 return stored_attendees.has_key(attendee) 486 else: 487 return True 488 489 def get_attendance(self, user=None, obj=None): 490 491 """ 492 Return the attendance attributes for 'user', or the current user if 493 'user' is not specified. 494 """ 495 496 attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE")) 497 return attendees.get(user or self.user) 498 499 def is_participating(self, user, as_organiser=False, obj=None): 500 501 """ 502 Return whether, subject to the 'user' indicating an identity and the 503 'as_organiser' status of that identity, the user concerned is actually 504 participating in the current object event. 505 """ 506 507 # Use any attendee property information for an organiser, not the 508 # organiser property attributes. 509 510 attr = self.get_attendance(user, obj=obj) 511 return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") != "DECLINED" 512 513 def get_overriding_transparency(self, user, as_organiser=False): 514 515 """ 516 Return the overriding transparency to be associated with the free/busy 517 records for an event, subject to the 'user' indicating an identity and 518 the 'as_organiser' status of that identity. 519 520 Where an identity is only an organiser and not attending, "ORG" is 521 returned. Otherwise, no overriding transparency is defined and None is 522 returned. 523 """ 524 525 attr = self.get_attendance(user) 526 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 527 528 def can_schedule(self, freebusy, periods): 529 530 """ 531 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 532 """ 533 534 return can_schedule(freebusy, periods, self.uid, self.recurrenceid) 535 536 def have_new_object(self, strict=True): 537 538 """ 539 Return whether the current object is new to the current user. 540 541 If 'strict' is specified and is a false value, the DTSTAMP test will be 542 ignored. This is useful in handling responses from attendees from 543 clients (like Claws Mail) that erase time information from DTSTAMP and 544 make it invalid. 545 """ 546 547 obj = self.get_stored_object_version() 548 549 # If found, compare SEQUENCE and potentially DTSTAMP. 550 551 if obj: 552 sequence = obj.get_value("SEQUENCE") 553 dtstamp = obj.get_value("DTSTAMP") 554 555 # If the request refers to an older version of the object, ignore 556 # it. 557 558 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 559 560 return True 561 562 def possibly_recurring_indefinitely(self): 563 564 "Return whether the object recurs indefinitely." 565 566 # Obtain the stored object to make sure that recurrence information 567 # is not being ignored. This might happen if a client sends a 568 # cancellation without the complete set of properties, for instance. 569 570 return self.obj.possibly_recurring_indefinitely() or \ 571 self.get_stored_object_version() and \ 572 self.get_stored_object_version().possibly_recurring_indefinitely() 573 574 # Constraint application on event periods. 575 576 def check_object(self): 577 578 "Check the object against any scheduling constraints." 579 580 permitted_values = self.get_permitted_values() 581 if not permitted_values: 582 return None 583 584 invalid = [] 585 586 for period in self.obj.get_periods(self.get_tzid()): 587 start = period.get_start() 588 end = period.get_end() 589 start_errors = check_permitted_values(start, permitted_values) 590 end_errors = check_permitted_values(end, permitted_values) 591 if start_errors or end_errors: 592 invalid.append((period.origin, start_errors, end_errors)) 593 594 return invalid 595 596 def correct_object(self): 597 598 "Correct the object according to any scheduling constraints." 599 600 permitted_values = self.get_permitted_values() 601 return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) 602 603 # Object retrieval. 604 605 def get_stored_object_version(self): 606 607 """ 608 Return the stored object to which the current object refers for the 609 current user. 610 """ 611 612 return self.get_stored_object(self.uid, self.recurrenceid) 613 614 def get_definitive_object(self, as_organiser): 615 616 """ 617 Return an object considered definitive for the current transaction, 618 using 'as_organiser' to select the current transaction's object if 619 false, or selecting a stored object if true. 620 """ 621 622 return not as_organiser and self.obj or self.get_stored_object_version() 623 624 def get_parent_object(self): 625 626 """ 627 Return the parent object to which the current object refers for the 628 current user. 629 """ 630 631 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 632 633 # Convenience methods for modifying free/busy collections. 634 635 def get_recurrence_start_point(self, recurrenceid): 636 637 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 638 639 return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) 640 641 def remove_from_freebusy(self, freebusy): 642 643 "Remove this event from the given 'freebusy' collection." 644 645 if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid: 646 remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid)) 647 648 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 649 650 """ 651 Remove from 'freebusy' any original recurrence from parent free/busy 652 details for the current object, if the current object is a specific 653 additional recurrence. Otherwise, remove all additional recurrence 654 information corresponding to 'recurrenceids', or if omitted, all 655 recurrences. 656 """ 657 658 if self.recurrenceid: 659 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 660 remove_affected_period(freebusy, self.uid, recurrenceid) 661 else: 662 # Remove obsolete recurrence periods. 663 664 remove_additional_periods(freebusy, self.uid, recurrenceids) 665 666 # Remove original periods affected by additional recurrences. 667 668 if recurrenceids: 669 for recurrenceid in recurrenceids: 670 recurrenceid = self.get_recurrence_start_point(recurrenceid) 671 remove_affected_period(freebusy, self.uid, recurrenceid) 672 673 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 674 675 """ 676 Update the 'freebusy' collection for this event with the periods and 677 transparency associated with the current object, subject to the 'user' 678 identity and the attendance details provided for them, indicating 679 whether the update is being done 'as_organiser' (for the organiser of 680 an event) or not. 681 682 If 'offer' is set to a true value, any free/busy updates will be tagged 683 with an expiry time. 684 """ 685 686 # Obtain the stored object if the current object is not issued by the 687 # organiser. Attendees do not have the opportunity to redefine the 688 # periods. 689 690 obj = self.get_definitive_object(as_organiser) 691 if not obj: 692 return 693 694 # Obtain the affected periods. 695 696 periods = self.get_periods(obj) 697 698 # Define an overriding transparency, the indicated event transparency, 699 # or the default transparency for the free/busy entry. 700 701 transp = self.get_overriding_transparency(user, as_organiser) or \ 702 obj.get_value("TRANSP") or \ 703 "OPAQUE" 704 705 # Calculate any expiry time. If no offer period is defined, do not 706 # record the offer periods. 707 708 if offer: 709 offer_period = self.get_offer_period() 710 if offer_period: 711 expires = get_timestamp(offer_period) 712 else: 713 return 714 else: 715 expires = None 716 717 # Perform the low-level update. 718 719 Client.update_freebusy(self, freebusy, periods, transp, 720 self.uid, self.recurrenceid, 721 obj.get_value("SUMMARY"), 722 obj.get_value("ORGANIZER"), 723 expires) 724 725 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 726 updating_other=False, offer=False): 727 728 """ 729 Update the 'freebusy' collection for the given 'user', indicating 730 whether the update is 'for_organiser' (being done for the organiser of 731 an event) or not, and whether it is 'updating_other' (meaning another 732 user's details). 733 734 If 'offer' is set to a true value, any free/busy updates will be tagged 735 with an expiry time. 736 """ 737 738 # Record in the free/busy details unless a non-participating attendee. 739 # Remove periods for non-participating attendees. 740 741 if offer or self.is_participating(user, for_organiser and not updating_other): 742 self.update_freebusy(freebusy, user, 743 for_organiser and not updating_other or 744 not for_organiser and updating_other, 745 offer 746 ) 747 else: 748 self.remove_from_freebusy(freebusy) 749 750 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 751 updating_other=False): 752 753 """ 754 Remove details from the 'freebusy' collection for the given 'user', 755 indicating whether the modification is 'for_organiser' (being done for 756 the organiser of an event) or not, and whether it is 'updating_other' 757 (meaning another user's details). 758 """ 759 760 # Remove from the free/busy details if a specified attendee. 761 762 if self.is_participating(user, for_organiser and not updating_other): 763 self.remove_from_freebusy(freebusy) 764 765 # Convenience methods for updating stored free/busy information received 766 # from other users. 767 768 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 769 770 """ 771 For the current user, record the free/busy information for another 772 'user', indicating whether the update is 'for_organiser' or not, thus 773 maintaining a separate record of their free/busy details. 774 """ 775 776 fn = fn or self.update_freebusy_for_participant 777 778 # A user does not store free/busy information for themself as another 779 # party. 780 781 if user == self.user: 782 return 783 784 self.acquire_lock() 785 try: 786 freebusy = self.store.get_freebusy_for_other(self.user, user) 787 fn(freebusy, user, for_organiser, True) 788 789 # Tidy up any obsolete recurrences. 790 791 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 792 self.store.set_freebusy_for_other(self.user, freebusy, user) 793 794 finally: 795 self.release_lock() 796 797 def update_freebusy_from_organiser(self, organiser): 798 799 "For the current user, record free/busy information from 'organiser'." 800 801 self.update_freebusy_from_participant(organiser, True) 802 803 def update_freebusy_from_attendees(self, attendees): 804 805 "For the current user, record free/busy information from 'attendees'." 806 807 for attendee in attendees.keys(): 808 self.update_freebusy_from_participant(attendee, False) 809 810 def remove_freebusy_from_organiser(self, organiser): 811 812 "For the current user, remove free/busy information from 'organiser'." 813 814 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 815 816 def remove_freebusy_from_attendees(self, attendees): 817 818 "For the current user, remove free/busy information from 'attendees'." 819 820 for attendee in attendees.keys(): 821 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 822 823 # Convenience methods for updating free/busy details at the event level. 824 825 def update_event_in_freebusy(self, for_organiser=True): 826 827 """ 828 Update free/busy information when handling an object, doing so for the 829 organiser of an event if 'for_organiser' is set to a true value. 830 """ 831 832 freebusy = self.store.get_freebusy(self.user) 833 834 # Obtain the attendance attributes for this user, if available. 835 836 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 837 838 # Remove original recurrence details replaced by additional 839 # recurrences, as well as obsolete additional recurrences. 840 841 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 842 self.store.set_freebusy(self.user, freebusy) 843 844 if self.publisher and self.is_sharing() and self.is_publishing(): 845 self.publisher.set_freebusy(self.user, freebusy) 846 847 # Update free/busy provider information if the event may recur 848 # indefinitely. 849 850 if self.possibly_recurring_indefinitely(): 851 self.store.append_freebusy_provider(self.user, self.obj) 852 853 return True 854 855 def remove_event_from_freebusy(self): 856 857 "Remove free/busy information when handling an object." 858 859 freebusy = self.store.get_freebusy(self.user) 860 861 self.remove_from_freebusy(freebusy) 862 self.remove_freebusy_for_recurrences(freebusy) 863 self.store.set_freebusy(self.user, freebusy) 864 865 if self.publisher and self.is_sharing() and self.is_publishing(): 866 self.publisher.set_freebusy(self.user, freebusy) 867 868 # Update free/busy provider information if the event may recur 869 # indefinitely. 870 871 if self.possibly_recurring_indefinitely(): 872 self.store.remove_freebusy_provider(self.user, self.obj) 873 874 def update_event_in_freebusy_offers(self): 875 876 "Update free/busy offers when handling an object." 877 878 freebusy = self.store.get_freebusy_offers(self.user) 879 880 # Obtain the attendance attributes for this user, if available. 881 882 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 883 884 # Remove original recurrence details replaced by additional 885 # recurrences, as well as obsolete additional recurrences. 886 887 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 888 self.store.set_freebusy_offers(self.user, freebusy) 889 890 return True 891 892 def remove_event_from_freebusy_offers(self): 893 894 "Remove free/busy offers when handling an object." 895 896 freebusy = self.store.get_freebusy_offers(self.user) 897 898 self.remove_from_freebusy(freebusy) 899 self.remove_freebusy_for_recurrences(freebusy) 900 self.store.set_freebusy_offers(self.user, freebusy) 901 902 return True 903 904 # Convenience methods for removing counter-proposals and updating the 905 # request queue. 906 907 def remove_request(self): 908 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 909 910 def remove_event(self): 911 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 912 913 def remove_counter(self, attendee): 914 self.remove_counters([attendee]) 915 916 def remove_counters(self, attendees): 917 for attendee in attendees: 918 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 919 920 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 921 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 922 923 # vim: tabstop=4 expandtab shiftwidth=4