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