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