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