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