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