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