1 #!/usr/bin/env python 2 3 """ 4 Common calendar client utilities. 5 6 Copyright (C) 2014, 2015, 2016, 2017, 2018 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 collections import OrderedDict 23 from datetime import datetime, timedelta 24 from imiptools.config import settings 25 from imiptools.data import check_delegation, get_address, \ 26 get_recurrence_periods, \ 27 get_sender_identities, get_uri, \ 28 get_window_end, is_new_object, make_freebusy, \ 29 make_uid, new_object, to_part, \ 30 update_attendees_with_delegates, \ 31 uri_dict, uri_parts, uri_values 32 from imiptools.dates import check_permitted_values, format_datetime, \ 33 get_datetime, get_default_timezone, \ 34 get_duration, get_time, get_timestamp, \ 35 to_datetime 36 from imiptools.i18n import get_translator 37 from imiptools.freebusy import SupportAttendee, SupportExpires 38 from imiptools.profile import Preferences 39 from imiptools.stores import get_store, get_publisher, get_journal 40 41 class Client: 42 43 "Common handler and manager methods." 44 45 default_window_size = 100 46 organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST" 47 48 def __init__(self, user, messenger=None, store=None, publisher=None, journal=None, 49 preferences_dir=None): 50 51 """ 52 Initialise a calendar client with the current 'user', plus any 53 'messenger', 'store', 'publisher' and 'journal' objects, indicating any 54 specific 'preferences_dir'. 55 """ 56 57 self.user = user 58 self.messenger = messenger 59 60 # The store is mandatory and will be initialised using defaults if not 61 # provided. 62 63 self.store = store or get_store() 64 65 # Journal and publisher objects are optional. 66 67 self.journal = journal 68 self.publisher = publisher 69 self.preferences_dir = preferences_dir 70 self.preferences = None 71 72 # Localise the messenger. 73 74 if self.messenger: 75 self.messenger.gettext = self.get_translator() 76 77 def get_store(self): 78 return self.store 79 80 def get_publisher(self): 81 return self.publisher 82 83 def get_journal(self): 84 return self.journal 85 86 # Store-related methods. 87 88 def acquire_lock(self): 89 self.store.acquire_lock(self.user) 90 91 def release_lock(self): 92 self.store.release_lock(self.user) 93 94 # Preferences-related methods. 95 96 def get_preferences(self): 97 if not self.preferences and self.user: 98 self.preferences = Preferences(self.user, self.preferences_dir) 99 return self.preferences 100 101 def get_locale(self): 102 prefs = self.get_preferences() 103 return prefs and prefs.get("LANG", "en", True) or "en" 104 105 def get_translator(self): 106 return get_translator([self.get_locale()]) 107 108 def get_user_attributes(self): 109 prefs = self.get_preferences() 110 return prefs and prefs.get_all(["CN"]) or {} 111 112 def get_tzid(self): 113 prefs = self.get_preferences() 114 return prefs and prefs.get("TZID") or get_default_timezone() 115 116 def get_window_size(self): 117 118 "Return the period window size as an integer." 119 120 prefs = self.get_preferences() 121 try: 122 return prefs and int(prefs.get("window_size")) or self.default_window_size 123 except (TypeError, ValueError): 124 return self.default_window_size 125 126 def get_window_start(self): 127 128 "Return the period window start as a datetime." 129 130 prefs = self.get_preferences() 131 tzid = self.get_tzid() 132 start = prefs and get_datetime(prefs.get("window_start"), {"TZID" : tzid}) 133 return isinstance(start, datetime) and start or start and to_datetime(start, tzid) 134 135 def get_window_end(self, size=None, start=None): 136 137 "Return the period window end as a datetime." 138 139 tzid = self.get_tzid() 140 return get_window_end(tzid, size or self.get_window_size(), 141 start or self.get_window_start()) 142 143 def is_participating(self): 144 145 "Return participation in the calendar system." 146 147 prefs = self.get_preferences() 148 return prefs and prefs.get("participating", settings["PARTICIPATING_DEFAULT"]) != "no" or False 149 150 def is_sharing(self): 151 152 "Return whether free/busy information is being generally shared." 153 154 prefs = self.get_preferences() 155 return prefs and prefs.get("freebusy_sharing", settings["SHARING_DEFAULT"]) == "share" or False 156 157 def is_bundling(self): 158 159 "Return whether free/busy information is being bundled in messages." 160 161 prefs = self.get_preferences() 162 return prefs and prefs.get("freebusy_bundling", settings["BUNDLING_DEFAULT"]) == "always" or False 163 164 def is_notifying(self): 165 166 "Return whether recipients are notified about free/busy payloads." 167 168 prefs = self.get_preferences() 169 return prefs and prefs.get("freebusy_messages", settings["NOTIFYING_DEFAULT"]) == "notify" or False 170 171 def is_publishing(self): 172 173 "Return whether free/busy information is being published as Web resources." 174 175 prefs = self.get_preferences() 176 return prefs and prefs.get("freebusy_publishing", settings["PUBLISHING_DEFAULT"]) == "publish" or False 177 178 def is_refreshing(self): 179 180 "Return whether a recipient supports requests to refresh event details." 181 182 prefs = self.get_preferences() 183 return prefs and prefs.get("event_refreshing", settings["REFRESHING_DEFAULT"]) == "always" or False 184 185 def allow_add(self): 186 return self.get_add_method_response() in ("add", "refresh") 187 188 def get_add_method_response(self): 189 prefs = self.get_preferences() 190 return prefs and prefs.get("add_method_response", settings["ADD_RESPONSE_DEFAULT"]) or "refresh" 191 192 def get_offer_period(self): 193 194 "Decode a specification in the iCalendar duration format." 195 196 prefs = self.get_preferences() 197 duration = prefs and prefs.get("freebusy_offers", settings["FREEBUSY_OFFER_DEFAULT"]) 198 199 # NOTE: Should probably report an error somehow if None. 200 201 return duration and get_duration(duration) or None 202 203 def get_organiser_replacement(self): 204 prefs = self.get_preferences() 205 return prefs and prefs.get("organiser_replacement", settings["ORGANISER_REPLACEMENT_DEFAULT"]) or "attendee" 206 207 def have_manager(self): 208 return settings["MANAGER_INTERFACE"] 209 210 def get_permitted_values(self): 211 212 """ 213 Decode a specification of one of the following forms... 214 215 <minute values> 216 <hour values>:<minute values> 217 <hour values>:<minute values>:<second values> 218 219 ...with each list of values being comma-separated. 220 """ 221 222 prefs = self.get_preferences() 223 permitted_values = prefs and prefs.get("permitted_times") 224 if permitted_values: 225 try: 226 l = [] 227 for component in permitted_values.split(":")[:3]: 228 if component: 229 l.append(map(int, component.split(","))) 230 else: 231 l.append(None) 232 233 # NOTE: Should probably report an error somehow. 234 235 except ValueError: 236 return None 237 else: 238 l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or []) 239 return l 240 else: 241 return None 242 243 # Common operations on calendar data. 244 245 def update_sender_attr(self, attr): 246 247 "Update the SENT-BY attribute of the 'attr' sender metadata." 248 249 if self.messenger: 250 if self.messenger.sender != get_address(self.user): 251 attr["SENT-BY"] = get_uri(self.messenger.sender) 252 elif attr.has_key("SENT-BY"): 253 del attr["SENT-BY"] 254 255 def get_periods(self, obj, explicit_only=False, future_only=False): 256 257 """ 258 Return periods for the given 'obj'. 259 260 If 'explicit_only' is set to a true value, only explicit periods will be 261 returned, not rule-based periods. 262 263 If 'future_only' is set to a true value, only future periods will be 264 returned, not all periods defined by an event starting in the past. 265 """ 266 267 return obj.get_periods( 268 start=(future_only and self.get_window_start() or None), 269 end=(not explicit_only and self.get_window_end() or None)) 270 271 def get_updated_periods(self, obj, explicit_only=False, future_only=False): 272 273 """ 274 Return the periods provided by 'obj' and associated recurrence 275 instances. Each original period is returned in a tuple with a 276 corresponding updated period which may be the same or which may be None 277 if the period is cancelled. A list of these tuples is returned. 278 279 If 'explicit_only' is set to a true value, only explicit periods will be 280 returned, not rule-based periods. 281 282 If 'future_only' is set to a true value, only future periods will be 283 returned, not all periods defined by an event starting in the past. 284 """ 285 286 self.store.update_event_from_recurrences(self.user, obj) 287 288 return obj.get_updated_periods( 289 start=(future_only and self.get_window_start() or None), 290 end=(not explicit_only and self.get_window_end() or None)) 291 292 # Store operations. 293 294 def get_stored_object(self, uid, recurrenceid, section=None, username=None): 295 296 """ 297 Return the stored object for the current user, with the given 'uid' and 298 'recurrenceid' from the given 'section' and for the given 'username' (if 299 specified), or from the standard object collection otherwise. 300 """ 301 302 if section == "counters": 303 obj = self.store.get_counter(self.user, username, uid, recurrenceid) 304 else: 305 obj = self.store.get_event(self.user, uid, recurrenceid, section) 306 307 # Set the fallback time zone. 308 309 if obj: 310 obj.set_tzid(self.get_tzid()) 311 312 return obj 313 314 # Free/busy operations. 315 316 def get_freebusy_part(self, freebusy=None): 317 318 """ 319 Return a message part containing free/busy information for the user, 320 either specified as 'freebusy' or obtained from the store directly. 321 """ 322 323 if self.is_sharing() and self.is_bundling(): 324 325 # Invent a unique identifier. 326 327 uid = make_uid(self.user) 328 329 freebusy = freebusy or self.store.get_freebusy(self.user) 330 331 user_attr = {} 332 self.update_sender_attr(user_attr) 333 return self.to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)]) 334 335 return None 336 337 def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None): 338 339 """ 340 Update the 'freebusy' collection with the given 'periods', indicating a 341 'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a 342 recurrence or the parent event. The 'summary' and 'organiser' must also 343 be provided. 344 345 An optional 'expires' datetime string can be provided to tag a free/busy 346 offer. 347 """ 348 349 # Add specific attendee information for certain collections. 350 351 if isinstance(freebusy, SupportAttendee): 352 freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, self.user) 353 354 # Add expiry datetime for certain collections. 355 356 elif isinstance(freebusy, SupportExpires): 357 freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires) 358 359 # Provide only the essential attributes for other collections. 360 361 else: 362 freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser) 363 364 # Preparation of content. 365 366 def to_part(self, method, fragments): 367 368 "Return an encoded MIME part for the given 'method' and 'fragments'." 369 370 return to_part(method, fragments, line_length=settings["CALENDAR_LINE_LENGTH"]) 371 372 def object_to_part(self, method, obj): 373 374 "Return an encoded MIME part for the given 'method' and 'obj'." 375 376 return obj.to_part(method, line_length=settings["CALENDAR_LINE_LENGTH"]) 377 378 # Preparation of messages communicating the state of events. 379 380 def get_message_parts(self, obj, method, attendee=None): 381 382 """ 383 Return a tuple containing a list of methods and a list of message parts, 384 with the parts collectively describing the given object 'obj' and its 385 recurrences, using 'method' as the means of publishing details (with 386 CANCEL being used to retract or remove details). 387 388 If 'attendee' is indicated, the attendee's participation will be taken 389 into account when generating the description. 390 """ 391 392 # Assume that the outcome will be composed of requests and 393 # cancellations. It would not seem completely bizarre to produce 394 # publishing messages if a refresh message was unprovoked. 395 396 responses = [] 397 methods = set() 398 399 # Get the parent event, add SENT-BY details to the organiser. 400 401 if not attendee or self.is_participating(attendee, obj=obj): 402 self.update_sender(obj) 403 responses.append(self.object_to_part(method, obj)) 404 methods.add(method) 405 406 # Get recurrences for parent events. 407 408 if not self.recurrenceid: 409 410 # Collect active and cancelled recurrences. 411 412 for rl, section, rmethod in [ 413 (self.store.get_active_recurrences(self.user, self.uid), None, method), 414 (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"), 415 ]: 416 417 for recurrenceid in rl: 418 419 # Get the recurrence, add SENT-BY details to the organiser. 420 421 obj = self.get_stored_object(self.uid, recurrenceid, section) 422 423 if not attendee or self.is_participating(attendee, obj=obj): 424 self.update_sender(obj) 425 responses.append(self.object_to_part(rmethod, obj)) 426 methods.add(rmethod) 427 428 return methods, responses 429 430 class ClientForObject(Client): 431 432 "A client maintaining a specific object." 433 434 def __init__(self, obj, user, messenger=None, store=None, publisher=None, 435 journal=None, preferences_dir=None): 436 Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir) 437 self.set_object(obj) 438 439 def set_object(self, obj): 440 441 "Set the current object to 'obj', obtaining metadata details." 442 443 # Set convenience attributes to null values if the object is null. 444 445 self.obj = obj 446 self.uid = obj and self.obj.get_uid() 447 self.recurrenceid = obj and self.obj.get_recurrenceid() 448 self.sequence = obj and self.obj.get_value("SEQUENCE") 449 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 450 451 # Set the fallback time zone. 452 453 if obj: 454 self.obj.set_tzid(self.get_tzid()) 455 456 def new_object(self, objtype): 457 458 "Initialise a new object for the client with the given 'objtype'." 459 460 self.set_object(new_object(objtype, self.user, self.get_user_attributes(), self.get_tzid())) 461 return self.obj 462 463 def load_object(self, uid, recurrenceid): 464 465 "Load the object with the given 'uid' and 'recurrenceid'." 466 467 # Obtain any parent event or separate recurrence. 468 469 obj = self.get_stored_object(uid, recurrenceid) 470 471 # Without a separate recurrence, obtain the parent. 472 473 if not obj and recurrenceid: 474 obj = self.get_stored_object(uid, None) 475 476 # With a parent, obtain the period for the recurrence. 477 478 if obj: 479 period = obj.has_recurrence_period(recurrenceid) 480 481 # With a period, make a separate recurrence. 482 483 if period: 484 obj = obj.make_recurrence(period) 485 486 self.set_object(obj) 487 return self.obj 488 489 def set_identity(self, method): 490 491 """ 492 Set the current user for the current object in the context of the given 493 'method'. It is usually set when initialising the handler, using the 494 recipient details, but outgoing messages do not reference the recipient 495 in this way. 496 """ 497 498 pass 499 500 def is_usable(self, method=None): 501 502 "Return whether the current object is usable with the given 'method'." 503 504 return True 505 506 def is_attendee(self): 507 508 "Return whether the current user is an attendee in the current object." 509 510 return self.obj.get_value_map("ATTENDEE").has_key(self.user) 511 512 def is_organiser(self): 513 514 """ 515 Return whether the current user is the organiser in the current object. 516 """ 517 518 return self.obj.get_uri("ORGANIZER") == self.user 519 520 def describes_recurrence_period(self): 521 522 """ 523 Return whether the current object describes a recurrence period defined 524 in its parent. 525 """ 526 527 parent = self.get_parent_object() 528 return parent and parent.has_recurrence_period(self.obj.get_recurrenceid()) 529 530 def get_recurrences(self, uid=None): 531 532 "Return the current object's recurrence identifiers." 533 534 return self.store.get_recurrences(self.user, uid or self.uid) 535 536 def get_periods(self, obj=None, explicit_only=False, future_only=False): 537 538 "Return the periods provided by the current object." 539 540 return Client.get_periods(self, obj or self.obj, explicit_only, future_only) 541 542 def get_updated_periods(self, obj=None): 543 544 """ 545 Return the periods provided by the current object and associated 546 recurrence instances. 547 """ 548 549 return Client.get_updated_periods(self, obj or self.obj) 550 551 # Common operations on calendar data. 552 553 def update_sender(self, obj=None): 554 555 """ 556 Update sender details in 'obj', or the current object if not indicated, 557 modifying the organiser attributes. 558 """ 559 560 obj = obj or self.obj 561 organiser, organiser_attr = obj.get_uri_item("ORGANIZER") 562 self.update_sender_attr(organiser_attr) 563 564 def update_senders(self, obj=None): 565 566 """ 567 Update sender details in 'obj', or the current object if not indicated, 568 removing SENT-BY attributes for attendees other than the current user if 569 those attributes give the URI of the calendar system. 570 """ 571 572 obj = obj or self.obj 573 calendar_uri = self.messenger and get_uri(self.messenger.sender) 574 575 for attendee, attendee_attr in obj.get_uri_items("ATTENDEE") or []: 576 577 # Fix up the SENT-BY attribute for this user. 578 579 if attendee == self.user: 580 self.update_sender_attr(attendee_attr) 581 582 # Remove any conflicting SENT-BY attributes for other users. 583 584 elif attendee_attr.get("SENT-BY") == calendar_uri: 585 del attendee_attr["SENT-BY"] 586 587 def get_sending_attendee(self): 588 589 "Return the attendee who sent the current object." 590 591 # Search for the sender of the message or the calendar system address. 592 593 senders = set(uri_values(self.senders or self.messenger and [self.messenger.sender] or [])) 594 595 if senders: 596 597 # Obtain a mapping from sender URI to attendee URI, where the sender 598 # is taken from the SENT-BY attribute if present, or from the 599 # attendee value otherwise. 600 601 sent_by = get_sender_identities(self.obj.get_uri_map("ATTENDEE")) 602 603 # Obtain the attendee for the first sender matching the SENT-BY or 604 # attendee value. 605 606 for sender in senders.intersection(sent_by.keys()): 607 return sent_by[sender][0] 608 609 return None 610 611 # Object update methods. 612 613 def update_recurrenceid(self): 614 615 """ 616 Update the RECURRENCE-ID in the current object, initialising it from 617 DTSTART. 618 """ 619 620 self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")] 621 self.recurrenceid = self.obj.get_recurrenceid() 622 623 def update_dtstamp(self, obj=None): 624 625 "Update the DTSTAMP in the current object or any given object 'obj'." 626 627 obj = obj or self.obj 628 self.dtstamp = obj.update_dtstamp() 629 630 def update_sequence(self, obj=None): 631 632 "Update the SEQUENCE in the current object or any given object 'obj'." 633 634 obj = obj or self.obj 635 obj.update_sequence(self.is_organiser()) 636 637 def merge_attendance(self, attendees): 638 639 """ 640 Merge attendance from the current object's 'attendees' into the version 641 stored for the current user. 642 """ 643 644 obj = self.get_stored_object_version() 645 646 if not obj or not self.have_new_object(): 647 return False 648 649 # Get attendee details in a usable form. 650 651 stored_attendees = obj.get_uri_map("ATTENDEE") 652 653 for attendee, attendee_attr in attendees.items(): 654 655 # Update attendance in the loaded object for any recognised 656 # attendees. 657 658 if stored_attendees.has_key(attendee): 659 stored_attendees[attendee] = attendee_attr 660 661 update_attendees_with_delegates(stored_attendees, attendees) 662 663 # Set the new details and store the object. 664 665 obj["ATTENDEE"] = stored_attendees.items() 666 667 # Set a specific recurrence or the complete event if not an additional 668 # occurrence. 669 670 return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node()) 671 672 def update_attendees(self, to_invite, to_cancel, to_modify): 673 674 """ 675 Update the attendees in the current object with the given 'to_invite', 676 'to_cancel' and 'to_modify' attendee mappings. 677 """ 678 679 attendees = self.obj.get_uri_items("ATTENDEE") or [] 680 attendee_map = OrderedDict(attendees) 681 682 # Normalise the identities. 683 684 to_invite = uri_dict(to_invite) 685 to_cancel = uri_dict(to_cancel) 686 to_modify = uri_dict(to_modify) 687 688 if self.is_organiser(): 689 690 # Remove uninvited attendees. 691 692 for attendee in to_cancel.keys(): 693 if attendee_map.has_key(attendee): 694 del attendee_map[attendee] 695 696 # Attendees (when countering) must only include the current user and 697 # any added attendees. 698 699 else: 700 attr = attendee_map.get(self.user) or self.get_user_attributes() 701 attendee_map = {self.user : attr} 702 703 # Update modified attendees. 704 705 for attendee, attr in to_modify.items(): 706 existing_attr = attendee_map.get(attendee) 707 if existing_attr: 708 existing_attr.update(attr) 709 710 # Add newly-invited attendees, applicable for organisers and attendees 711 # (when countering). 712 713 for attendee, attr in to_invite.items(): 714 if not attendee_map.has_key(attendee): 715 716 # Only the organiser can reset the participation attributes. 717 718 if self.is_organiser() and attendee != self.user: 719 attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}) 720 721 attendee_map[attendee] = attr 722 723 self.obj["ATTENDEE"] = attendee_map.items() 724 725 def update_participation(self, partstat=None): 726 727 """ 728 Update the participation in the current object of the user with the 729 given 'partstat'. 730 """ 731 732 attendee_attr = self.obj.get_uri_map("ATTENDEE").get(self.user) 733 734 if not attendee_attr: 735 return None 736 737 # Set the participation and remove any request for response indicator. 738 739 if partstat: 740 attendee_attr["PARTSTAT"] = partstat 741 if attendee_attr.has_key("RSVP"): 742 del attendee_attr["RSVP"] 743 744 return attendee_attr 745 746 def update_event_version(self, changed=False): 747 748 """ 749 Update the event version information and details for sending. Where 750 'changed' is set to a true value for the attendee, the attendee list 751 will be processed and preserved; otherwise, the attendee replying will 752 be retained and the others removed from the object. 753 """ 754 755 if self.is_organiser(): 756 self.update_sender() 757 758 # Process attendee SENT-BY usage, timestamp and sequence details 759 # appropriately for the sender's role. 760 761 self.update_senders() 762 self.update_dtstamp() 763 self.update_sequence() 764 765 return True 766 767 def update_event_from_periods(self, to_set, to_exclude): 768 769 """ 770 Set the periods in any redefined event from the 'to_set' list, excluding 771 the main period if it appears in 'to_exclude'. 772 """ 773 774 if to_set: 775 self.obj.set_periods(to_set) 776 777 if to_exclude: 778 self.obj.update_exceptions(to_exclude, to_set or []) 779 780 # General message generation methods. 781 782 def get_recipients(self, obj=None): 783 784 """ 785 Return recipients for 'obj' (or the current object) dependent on the 786 current user's role. 787 """ 788 789 obj = obj or self.obj 790 791 organiser = obj.get_uri("ORGANIZER") 792 attendees = obj.get_uri_values("ATTENDEE") 793 794 # As organiser, send an invitation to attendees, excluding oneself if 795 # also attending. The updated event will be saved by the outgoing 796 # handler. 797 798 if self.is_organiser(): 799 if not attendees: 800 return [] 801 else: 802 l = [] 803 for attendee in attendees: 804 if attendee != self.user: 805 l.append(get_address(attendee)) 806 return l 807 else: 808 return [get_address(organiser)] 809 810 def attach_freebusy(self, parts): 811 812 """ 813 Since the outgoing handler updates this user's free/busy details, the 814 stored details will probably not have the updated details straight away, 815 so we update our copy for serialisation as the bundled free/busy object. 816 """ 817 818 freebusy = self.store.get_freebusy(self.user).copy() 819 self.update_freebusy(freebusy, self.user, self.is_organiser()) 820 821 # Bundle free/busy information if appropriate. 822 823 part = self.get_freebusy_part(freebusy) 824 if part: 825 parts.append(part) 826 827 def make_message(self, parts, recipients, bcc_sender=False): 828 829 """ 830 Send the given 'parts' to the appropriate 'recipients', also sending a 831 copy to the sender. 832 """ 833 834 if not self.messenger: 835 return None 836 837 # Update and attach bundled free/busy details. 838 839 self.attach_freebusy(parts) 840 841 if not bcc_sender: 842 return self.messenger.make_outgoing_message(parts, recipients) 843 else: 844 sender = get_address(self.user) 845 return self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) 846 847 def send_message(self, message, recipients, bcc_sender=False): 848 849 """ 850 Send 'message' to 'recipients', explicitly specifying the sender as an 851 outgoing BCC recipient if 'bcc_sender' is set, since the generic 852 calendar user will be the actual sender. 853 """ 854 855 if not recipients and not bcc_sender or not self.messenger: 856 return 857 858 if not bcc_sender: 859 self.messenger.sendmail(recipients, message.as_string()) 860 else: 861 sender = get_address(self.user) 862 self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) 863 864 def make_message_for_self(self, parts): 865 866 "Send 'message' to the current user." 867 868 if not self.messenger: 869 return None 870 871 sender = get_address(self.user) 872 return self.messenger.make_outgoing_message(parts, [sender]) 873 874 def send_message_to_self(self, message): 875 876 "Send 'message' to the current user." 877 878 if not self.messenger: 879 return 880 881 sender = get_address(self.user) 882 self.messenger.sendmail([sender], message.as_string()) 883 884 # Specific message generation methods. 885 886 def get_rescheduled_objects(self, periods, method): 887 888 "Return objects describing rescheduled 'periods' for 'method'." 889 890 objects = [] 891 892 if periods: 893 894 # Process each period, attempting to update existing recurrences or 895 # creating new ones. 896 897 for p in periods: 898 if not p.origin: 899 continue 900 901 # Attempt to find an existing recurrence. 902 903 obj = self.get_stored_object(self.uid, p.get_recurrenceid()) 904 905 # Make a new separate recurrence if necessary. 906 907 if not obj: 908 909 # Added methods do not employ a recurrence identifier. 910 911 obj = self.obj.make_recurrence(p, method != "ADD") 912 913 # Update any sequence number if the period has changed from any 914 # stored version. 915 # NOTE: Attendance changes would also be propagated and have 916 # NOTE: this effect. 917 918 else: 919 main = obj.get_main_period() 920 if main != p: 921 self.update_sequence(obj) 922 923 # Set specific recurrence information. 924 925 obj.set_datetime("DTSTART", p.get_start()) 926 obj.set_datetime("DTEND", p.get_end()) 927 928 objects.append(obj) 929 930 return objects 931 932 def get_rescheduled_parts(self, periods, method): 933 934 """ 935 Return message parts describing rescheduled 'periods' affected by 'method'. 936 """ 937 938 rescheduled_parts = [] 939 940 for obj in self.get_rescheduled_objects(periods, method): 941 rescheduled_parts.append(self.object_to_part(method, obj)) 942 943 return rescheduled_parts 944 945 def make_update_message(self, recipients, update_parent=False, 946 to_unschedule=None, to_reschedule=None, 947 all_unscheduled=None, all_rescheduled=None, 948 to_add=None): 949 950 """ 951 Prepare event updates from the organiser of an event for the given 952 'recipients', including the parent event if 'update_parent' is set to a 953 true value. 954 955 Additional parts are provided by the 'to_unschedule' and 'to_reschedule' 956 collections. Alternatively, where the parent event is being updated, the 957 'all_unscheduled' and 'all_rescheduled' period collections are included. 958 959 The 'to_add' period collection augments the existing periods. 960 """ 961 962 parts = [] 963 964 if update_parent: 965 parts.append(self.object_to_part("REQUEST", self.obj)) 966 unscheduled = all_unscheduled 967 rescheduled = all_rescheduled 968 else: 969 unscheduled = to_unschedule 970 rescheduled = to_reschedule 971 972 parts += self.get_rescheduled_parts(unscheduled, "CANCEL") 973 parts += self.get_rescheduled_parts(rescheduled, "REQUEST") 974 parts += self.get_rescheduled_parts(to_add, "ADD") 975 return self.make_message(parts, recipients) 976 977 def make_self_update_message(self, all_unscheduled=None, all_rescheduled=None, 978 to_add=None): 979 980 """ 981 Prepare event updates to be sent from the organiser of an event to 982 themself. 983 """ 984 985 parts = [] 986 987 # Do not publish the event if it is a recurrence and is cancelled or 988 # rescheduled. 989 990 main = self.obj.get_recurrenceid() and self.obj.get_main_period() or None 991 992 if not main or (not main in all_unscheduled and not main in all_rescheduled): 993 parts += [self.object_to_part("PUBLISH", self.obj)] 994 995 parts += self.get_rescheduled_parts(all_unscheduled, "CANCEL") 996 parts += self.get_rescheduled_parts(all_rescheduled, "PUBLISH") 997 parts += self.get_rescheduled_parts(to_add, "ADD") 998 return self.make_message_for_self(parts) 999 1000 def make_response_message(self, recipients, update_parent=False, 1001 all_rescheduled=None, to_reschedule=None): 1002 1003 """ 1004 Prepare a response to 'recipients', including the parent event if 1005 'update_parent' is set to a true value, incorporating 'all_rescheduled' 1006 periods, of which there may be indicated periods 'to_reschedule'. 1007 """ 1008 1009 parts = [self.object_to_part(update_parent and "COUNTER" or "REPLY", self.obj)] 1010 1011 # Determine existing replaced periods that are not newly rescheduled. 1012 1013 rescheduled_unmodified = set(all_rescheduled or []).difference(to_reschedule or []) 1014 1015 if rescheduled_unmodified: 1016 parts += self.get_rescheduled_parts(rescheduled_unmodified, update_parent and "COUNTER" or "REPLY") 1017 1018 # Suggest details for newly rescheduled periods. 1019 1020 if to_reschedule: 1021 parts += self.get_rescheduled_parts(to_reschedule, "COUNTER") 1022 1023 return self.make_message(parts, recipients, bcc_sender=True) 1024 1025 def make_cancel_message(self, to_cancel=None): 1026 1027 """ 1028 Prepare an event cancellation message involving the participants in the 1029 'to_cancel' mapping or involving all participants if 'to_cancel' is not 1030 specified or empty. 1031 """ 1032 1033 obj = self.obj.copy() 1034 1035 if to_cancel: 1036 obj["ATTENDEE"] = to_cancel.items() 1037 1038 recipients = self.get_recipients(obj) 1039 1040 parts = [self.object_to_part("CANCEL", obj)] 1041 return self.make_message(parts, recipients) 1042 1043 def make_cancel_message_for_self(self): 1044 1045 "Prepare an event cancellation for the current user." 1046 1047 parts = [self.object_to_part("CANCEL", self.obj)] 1048 return self.make_message_for_self(parts) 1049 1050 def make_freebusy_message(self): 1051 1052 "Prepare a free/busy request for the main period." 1053 1054 user_attr = {} 1055 self.update_sender_attr(user_attr) 1056 1057 attributes = self.obj.get_items("ATTENDEE") 1058 period = self.obj.get_main_period() 1059 1060 recipients = self.get_recipients() 1061 1062 # NOTE: Should choose a different UID to any event. 1063 1064 fb = make_freebusy(None, self.uid, self.user, user_attr, attributes, period) 1065 parts = [self.to_part("REQUEST", [fb])] 1066 return self.make_message(parts, recipients) 1067 1068 # Action methods. 1069 1070 def send_declined_counter_to_attendee(self, attendee): 1071 1072 "Send a declined counter-proposal to 'attendee'." 1073 1074 # Obtain the counter-proposal for the attendee. 1075 1076 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 1077 if not obj: 1078 return 1079 1080 self.update_senders(obj) 1081 obj.update_dtstamp() 1082 obj.update_sequence() 1083 1084 parts = [self.object_to_part("DECLINECOUNTER", obj)] 1085 1086 # Create and send the response. 1087 1088 recipients = self.get_recipients(obj) 1089 message = self.make_message(parts, recipients, bcc_sender=True) 1090 self.send_message(message, recipients, bcc_sender=True) 1091 1092 def send_response_to_organiser(self, all_rescheduled=None, to_reschedule=None, 1093 changed=False): 1094 1095 """ 1096 Send a response to the organiser describing attendance and proposed 1097 amendments to the event. 1098 1099 If 'all_rescheduled' is specified, it provides details of separate 1100 recurrence instances for which a response needs to be generated. 1101 1102 If 'to_reschedule' provides rescheduled periods, these will be sent as 1103 counter-proposals. 1104 1105 If 'changed' is set to a true value, a counter-proposal will be sent for 1106 the entire event instead of a reply. 1107 """ 1108 1109 recipients = self.get_recipients() 1110 message = self.make_response_message(recipients, all_rescheduled, 1111 to_reschedule, changed) 1112 self.send_message(message, recipients, bcc_sender=True) 1113 1114 def send_update_to_recipients(self, to_unschedule=None, to_reschedule=None): 1115 1116 """ 1117 Send cancellations for each of the recurrences 'to_unschedule' along 1118 with modifications for each of the recurrences 'to_reschedule'. 1119 """ 1120 1121 recipients = self.get_recipients() 1122 message = self.make_update_message(recipients, to_unschedule, to_reschedule) 1123 self.send_message(message, recipients) 1124 1125 def send_publish_to_self(self, all_unscheduled=None, all_rescheduled=None): 1126 1127 """ 1128 Send published event details incorporating 'all_unscheduled' and 1129 'all_rescheduled' periods. 1130 """ 1131 1132 # Since the organiser can update the SEQUENCE but this can leave any 1133 # mail/calendar client lagging, issue a PUBLISH message to the 1134 # user's address. 1135 1136 recipients = self.get_recipients() 1137 message = self.make_self_update_message(all_unscheduled, all_rescheduled) 1138 self.send_message_to_self(message) 1139 1140 def send_cancel_to_recipients(self, to_cancel=None): 1141 1142 "Send a cancellation to all uninvited attendees in 'to_cancel'." 1143 1144 message = self.make_cancel_message(to_cancel) 1145 self.send_message(message, to_cancel.keys()) 1146 1147 def send_cancel_to_self(self): 1148 1149 "Issue a CANCEL message to the user's address." 1150 1151 message = self.make_cancel_message_for_self() 1152 self.send_message_to_self(message) 1153 1154 # Object-related tests. 1155 1156 def is_recognised_organiser(self, organiser): 1157 1158 """ 1159 Return whether the given 'organiser' is recognised from 1160 previously-received details. If no stored details exist, True is 1161 returned. 1162 """ 1163 1164 obj = self.get_stored_object_version() 1165 if obj: 1166 stored_organiser = obj.get_uri("ORGANIZER") 1167 return stored_organiser == organiser 1168 else: 1169 return True 1170 1171 def is_recognised_attendee(self, attendee): 1172 1173 """ 1174 Return whether the given 'attendee' is recognised from 1175 previously-received details. If no stored details exist, True is 1176 returned. 1177 """ 1178 1179 obj = self.get_stored_object_version() 1180 if obj: 1181 stored_attendees = obj.get_uri_map("ATTENDEE") 1182 return stored_attendees.has_key(attendee) 1183 else: 1184 return True 1185 1186 def get_attendance(self, user=None, obj=None): 1187 1188 """ 1189 Return the attendance attributes for 'user', or the current user if 1190 'user' is not specified. 1191 """ 1192 1193 attendees = (obj or self.obj).get_uri_map("ATTENDEE") 1194 return attendees.get(user or self.user) 1195 1196 def is_participating(self, user, as_organiser=False, obj=None): 1197 1198 """ 1199 Return whether, subject to the 'user' indicating an identity and the 1200 'as_organiser' status of that identity, the user concerned is actually 1201 participating in the current object event. 1202 """ 1203 1204 # Use any attendee property information for an organiser, not the 1205 # organiser property attributes. 1206 1207 attr = self.get_attendance(user, obj) 1208 return as_organiser or attr is not None and not attr or \ 1209 attr and attr.get("PARTSTAT") not in ("DECLINED", "DELEGATED", "NEEDS-ACTION") 1210 1211 def has_indicated_attendance(self, user=None, obj=None): 1212 1213 """ 1214 Return whether the given 'user' (or the current user if not specified) 1215 has indicated attendance in the given 'obj' (or the current object if 1216 not specified). 1217 """ 1218 1219 attr = self.get_attendance(user, obj) 1220 return attr and attr.get("PARTSTAT") not in (None, "NEEDS-ACTION") 1221 1222 def get_overriding_transparency(self, user, as_organiser=False): 1223 1224 """ 1225 Return the overriding transparency to be associated with the free/busy 1226 records for an event, subject to the 'user' indicating an identity and 1227 the 'as_organiser' status of that identity. 1228 1229 Where an identity is only an organiser and not attending, "ORG" is 1230 returned. Otherwise, no overriding transparency is defined and None is 1231 returned. 1232 """ 1233 1234 attr = self.get_attendance(user) 1235 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 1236 1237 def can_schedule(self, freebusy, periods): 1238 1239 """ 1240 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 1241 """ 1242 1243 return freebusy.can_schedule(periods, self.uid, self.recurrenceid) 1244 1245 def have_new_object(self, strict=True): 1246 1247 """ 1248 Return whether the current object is new to the current user. 1249 1250 If 'strict' is specified and is a false value, the DTSTAMP test will be 1251 ignored. This is useful in handling responses from attendees from 1252 clients (like Claws Mail) that erase time information from DTSTAMP and 1253 make it invalid. 1254 """ 1255 1256 obj = self.get_stored_object_version() 1257 1258 # If found, compare SEQUENCE and potentially DTSTAMP. 1259 1260 if obj: 1261 sequence = obj.get_value("SEQUENCE") 1262 dtstamp = obj.get_value("DTSTAMP") 1263 1264 # If the request refers to an older version of the object, ignore 1265 # it. 1266 1267 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 1268 1269 return True 1270 1271 def possibly_recurring_indefinitely(self): 1272 1273 "Return whether the object recurs indefinitely." 1274 1275 # Obtain the stored object to make sure that recurrence information 1276 # is not being ignored. This might happen if a client sends a 1277 # cancellation without the complete set of properties, for instance. 1278 1279 return self.obj.possibly_recurring_indefinitely() or \ 1280 self.get_stored_object_version() and \ 1281 self.get_stored_object_version().possibly_recurring_indefinitely() 1282 1283 # Constraint application on event periods. 1284 1285 def check_object(self): 1286 1287 "Check the object against any scheduling constraints." 1288 1289 permitted_values = self.get_permitted_values() 1290 if not permitted_values: 1291 return None 1292 1293 invalid = [] 1294 1295 for period in self.obj.get_periods(): 1296 errors = period.check_permitted(permitted_values) 1297 if errors: 1298 start_errors, end_errors = errors 1299 invalid.append((period.origin, start_errors, end_errors)) 1300 1301 return invalid 1302 1303 def correct_object(self): 1304 1305 "Correct the object according to any scheduling constraints." 1306 1307 permitted_values = self.get_permitted_values() 1308 return permitted_values and self.obj.correct_object(permitted_values) 1309 1310 def correct_period(self, period): 1311 1312 "Correct 'period' according to any scheduling constraints." 1313 1314 permitted_values = self.get_permitted_values() 1315 if not permitted_values: 1316 return period 1317 else: 1318 return period.get_corrected(permitted_values) 1319 1320 # Object retrieval. 1321 1322 def get_stored_object_version(self): 1323 1324 """ 1325 Return the stored object to which the current object refers for the 1326 current user. 1327 """ 1328 1329 return self.get_stored_object(self.uid, self.recurrenceid) 1330 1331 def get_definitive_object(self, as_organiser): 1332 1333 """ 1334 Return an object considered definitive for the current transaction, 1335 using 'as_organiser' to select the current transaction's object if 1336 false, or selecting a stored object if true. 1337 """ 1338 1339 return not as_organiser and self.obj or self.get_stored_object_version() 1340 1341 def get_parent_object(self): 1342 1343 """ 1344 Return the parent object to which the current object refers for the 1345 current user. 1346 """ 1347 1348 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 1349 1350 # Convenience methods for modifying free/busy collections. 1351 1352 def get_recurrence_start_point(self, recurrenceid): 1353 1354 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 1355 1356 return self.obj.get_recurrence_start_point(recurrenceid) 1357 1358 def remove_from_freebusy(self, freebusy, participant=None): 1359 1360 """ 1361 Remove this event from the given 'freebusy' collection. If 'participant' 1362 is specified, only remove this event if the participant is attending. 1363 """ 1364 1365 removed = freebusy.remove_event_periods(self.uid, self.recurrenceid, participant) 1366 if not removed and self.recurrenceid: 1367 return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid), participant) 1368 else: 1369 return removed 1370 1371 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 1372 1373 """ 1374 Remove from 'freebusy' any original recurrence from parent free/busy 1375 details for the current object, if the current object is a specific 1376 additional recurrence. Otherwise, remove all additional recurrence 1377 information corresponding to 'recurrenceids', or if omitted, all 1378 recurrences. 1379 """ 1380 1381 if self.recurrenceid: 1382 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 1383 freebusy.remove_affected_period(self.uid, recurrenceid) 1384 else: 1385 # Remove obsolete recurrence periods. 1386 1387 freebusy.remove_additional_periods(self.uid, recurrenceids) 1388 1389 # Remove original periods affected by additional recurrences. 1390 1391 if recurrenceids: 1392 for recurrenceid in recurrenceids: 1393 recurrenceid = self.get_recurrence_start_point(recurrenceid) 1394 freebusy.remove_affected_period(self.uid, recurrenceid) 1395 1396 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 1397 1398 """ 1399 Update the 'freebusy' collection for this event with the periods and 1400 transparency associated with the current object, subject to the 'user' 1401 identity and the attendance details provided for them, indicating 1402 whether the update is being done 'as_organiser' (for the organiser of 1403 an event) or not. 1404 1405 If 'offer' is set to a true value, any free/busy updates will be tagged 1406 with an expiry time. 1407 """ 1408 1409 # Obtain the stored object if the current object is not issued by the 1410 # organiser. Attendees do not have the opportunity to redefine the 1411 # periods. 1412 1413 obj = self.get_definitive_object(as_organiser) 1414 if not obj: 1415 return 1416 1417 # Obtain the affected periods. 1418 1419 periods = self.get_periods(obj, future_only=True) 1420 1421 # Define an overriding transparency, the indicated event transparency, 1422 # or the default transparency for the free/busy entry. 1423 1424 transp = self.get_overriding_transparency(user, as_organiser) or \ 1425 obj.get_value("TRANSP") or \ 1426 "OPAQUE" 1427 1428 # Calculate any expiry time. If no offer period is defined, do not 1429 # record the offer periods. 1430 1431 if offer: 1432 offer_period = self.get_offer_period() 1433 if offer_period: 1434 expires = get_timestamp(offer_period) 1435 else: 1436 return 1437 else: 1438 expires = None 1439 1440 # Perform the low-level update. 1441 1442 Client.update_freebusy(self, freebusy, periods, transp, 1443 self.uid, self.recurrenceid, 1444 obj.get_value("SUMMARY"), 1445 obj.get_uri("ORGANIZER"), 1446 expires) 1447 1448 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 1449 updating_other=False, offer=False): 1450 1451 """ 1452 Update the 'freebusy' collection for the given 'user', indicating 1453 whether the update is 'for_organiser' (being done for the organiser of 1454 an event) or not, and whether it is 'updating_other' (meaning another 1455 user's details). 1456 1457 If 'offer' is set to a true value, any free/busy updates will be tagged 1458 with an expiry time. 1459 """ 1460 1461 # Record in the free/busy details unless a non-participating attendee. 1462 # Remove periods for non-participating attendees. 1463 1464 if offer or self.is_participating(user, for_organiser and not updating_other): 1465 self.update_freebusy(freebusy, user, 1466 for_organiser and not updating_other or 1467 not for_organiser and updating_other, 1468 offer 1469 ) 1470 else: 1471 self.remove_from_freebusy(freebusy) 1472 1473 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 1474 updating_other=False): 1475 1476 """ 1477 Remove details from the 'freebusy' collection for the given 'user', 1478 indicating whether the modification is 'for_organiser' (being done for 1479 the organiser of an event) or not, and whether it is 'updating_other' 1480 (meaning another user's details). 1481 """ 1482 1483 # Remove from the free/busy details if a specified attendee. 1484 1485 if self.is_participating(user, for_organiser and not updating_other): 1486 self.remove_from_freebusy(freebusy) 1487 1488 # Convenience methods for updating stored free/busy information received 1489 # from other users. 1490 1491 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 1492 1493 """ 1494 For the current user, record the free/busy information for another 1495 'user', indicating whether the update is 'for_organiser' or not, thus 1496 maintaining a separate record of their free/busy details. 1497 """ 1498 1499 fn = fn or self.update_freebusy_for_participant 1500 1501 # A user does not store free/busy information for themself as another 1502 # party. 1503 1504 if user == self.user: 1505 return 1506 1507 self.acquire_lock() 1508 try: 1509 freebusy = self.store.get_freebusy_for_other_for_update(self.user, user) 1510 fn(freebusy, user, for_organiser, True) 1511 1512 # Tidy up any obsolete recurrences. 1513 1514 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1515 self.store.set_freebusy_for_other(self.user, freebusy, user) 1516 1517 finally: 1518 self.release_lock() 1519 1520 def update_freebusy_from_organiser(self, organiser): 1521 1522 "For the current user, record free/busy information from 'organiser'." 1523 1524 self.update_freebusy_from_participant(organiser, True) 1525 1526 def update_freebusy_from_attendees(self, attendees): 1527 1528 "For the current user, record free/busy information from 'attendees'." 1529 1530 obj = self.get_stored_object_version() 1531 1532 if not obj or not self.have_new_object(): 1533 return False 1534 1535 # Filter out unrecognised attendees. 1536 1537 attendees = set(attendees).intersection(obj.get_uri_values("ATTENDEE")) 1538 1539 for attendee in attendees: 1540 self.update_freebusy_from_participant(attendee, False) 1541 1542 return True 1543 1544 def remove_freebusy_from_organiser(self, organiser): 1545 1546 "For the current user, remove free/busy information from 'organiser'." 1547 1548 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 1549 1550 def remove_freebusy_from_attendees(self, attendees): 1551 1552 "For the current user, remove free/busy information from 'attendees'." 1553 1554 for attendee in attendees.keys(): 1555 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 1556 1557 # Convenience methods for updating free/busy details at the event level. 1558 1559 def update_event_in_freebusy(self, for_organiser=True): 1560 1561 """ 1562 Update free/busy information when handling an object, doing so for the 1563 organiser of an event if 'for_organiser' is set to a true value. 1564 """ 1565 1566 freebusy = self.store.get_freebusy_for_update(self.user) 1567 1568 # Obtain the attendance attributes for this user, if available. 1569 1570 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 1571 1572 # Remove original recurrence details replaced by additional 1573 # recurrences, as well as obsolete additional recurrences. 1574 1575 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1576 self.store.set_freebusy(self.user, freebusy) 1577 1578 if self.publisher and self.is_sharing() and self.is_publishing(): 1579 self.publisher.set_freebusy(self.user, freebusy) 1580 1581 # Update free/busy provider information if the event may recur 1582 # indefinitely. 1583 1584 if self.possibly_recurring_indefinitely(): 1585 self.store.append_freebusy_provider(self.user, self.obj) 1586 1587 return True 1588 1589 def remove_event_from_freebusy(self): 1590 1591 "Remove free/busy information when handling an object." 1592 1593 freebusy = self.store.get_freebusy_for_update(self.user) 1594 1595 self.remove_from_freebusy(freebusy) 1596 self.remove_freebusy_for_recurrences(freebusy) 1597 self.store.set_freebusy(self.user, freebusy) 1598 1599 if self.publisher and self.is_sharing() and self.is_publishing(): 1600 self.publisher.set_freebusy(self.user, freebusy) 1601 1602 # Update free/busy provider information if the event may recur 1603 # indefinitely. 1604 1605 if self.possibly_recurring_indefinitely(): 1606 self.store.remove_freebusy_provider(self.user, self.obj) 1607 1608 def update_event_in_freebusy_offers(self): 1609 1610 "Update free/busy offers when handling an object." 1611 1612 freebusy = self.store.get_freebusy_offers_for_update(self.user) 1613 1614 # Obtain the attendance attributes for this user, if available. 1615 1616 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 1617 1618 # Remove original recurrence details replaced by additional 1619 # recurrences, as well as obsolete additional recurrences. 1620 1621 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1622 self.store.set_freebusy_offers(self.user, freebusy) 1623 1624 return True 1625 1626 def remove_event_from_freebusy_offers(self): 1627 1628 "Remove free/busy offers when handling an object." 1629 1630 freebusy = self.store.get_freebusy_offers_for_update(self.user) 1631 1632 self.remove_from_freebusy(freebusy) 1633 self.remove_freebusy_for_recurrences(freebusy) 1634 self.store.set_freebusy_offers(self.user, freebusy) 1635 1636 return True 1637 1638 # Convenience methods for removing counter-proposals and updating the 1639 # request queue. 1640 1641 def remove_request(self): 1642 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1643 1644 def remove_event(self): 1645 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 1646 1647 def remove_counter(self, attendee): 1648 self.remove_counters([attendee]) 1649 1650 def remove_counters(self, attendees): 1651 for attendee in attendees: 1652 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 1653 1654 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 1655 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1656 1657 # vim: tabstop=4 expandtab shiftwidth=4