1 #!/usr/bin/env python 2 3 """ 4 Common calendar client utilities. 5 6 Copyright (C) 2014, 2015, 2016, 2017 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 self.obj = obj 444 self.uid = obj and self.obj.get_uid() 445 self.recurrenceid = obj and self.obj.get_recurrenceid() 446 self.sequence = obj and self.obj.get_value("SEQUENCE") 447 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 448 449 # Set the fallback time zone. 450 451 if obj: 452 self.obj.set_tzid(self.get_tzid()) 453 454 def new_object(self, objtype): 455 456 "Initialise a new object for the client with the given 'objtype'." 457 458 self.set_object(new_object(objtype, self.user, self.get_user_attributes(), self.get_tzid())) 459 return self.obj 460 461 def load_object(self, uid, recurrenceid): 462 463 "Load the object with the given 'uid' and 'recurrenceid'." 464 465 self.set_object(self.get_stored_object(uid, recurrenceid)) 466 return self.obj 467 468 def set_identity(self, method): 469 470 """ 471 Set the current user for the current object in the context of the given 472 'method'. It is usually set when initialising the handler, using the 473 recipient details, but outgoing messages do not reference the recipient 474 in this way. 475 """ 476 477 pass 478 479 def is_usable(self, method=None): 480 481 "Return whether the current object is usable with the given 'method'." 482 483 return True 484 485 def is_attendee(self): 486 487 "Return whether the current user is an attendee in the current object." 488 489 return self.obj.get_value_map("ATTENDEE").has_key(self.user) 490 491 def is_organiser(self): 492 493 """ 494 Return whether the current user is the organiser in the current object. 495 """ 496 497 return self.obj.get_uri("ORGANIZER") == self.user 498 499 def is_recurrence(self): 500 501 "Return whether the current object is a recurrence of its parent." 502 503 parent = self.get_parent_object() 504 return parent and parent.has_recurrence(self.obj.get_recurrenceid()) 505 506 def get_recurrences(self, uid=None): 507 508 "Return the current object's recurrence identifiers." 509 510 return self.store.get_recurrences(self.user, uid or self.uid) 511 512 def get_periods(self, obj=None, explicit_only=False, future_only=False): 513 514 "Return the periods provided by the current object." 515 516 return Client.get_periods(self, obj or self.obj, explicit_only, future_only) 517 518 def get_updated_periods(self, obj=None): 519 520 """ 521 Return the periods provided by the current object and associated 522 recurrence instances. 523 """ 524 525 return Client.get_updated_periods(self, obj or self.obj) 526 527 # Common operations on calendar data. 528 529 def update_sender(self, obj=None): 530 531 """ 532 Update sender details in 'obj', or the current object if not indicated, 533 modifying the organiser attributes. 534 """ 535 536 obj = obj or self.obj 537 organiser, organiser_attr = obj.get_uri_item("ORGANIZER") 538 self.update_sender_attr(organiser_attr) 539 540 def update_senders(self, obj=None): 541 542 """ 543 Update sender details in 'obj', or the current object if not indicated, 544 removing SENT-BY attributes for attendees other than the current user if 545 those attributes give the URI of the calendar system. 546 """ 547 548 obj = obj or self.obj 549 calendar_uri = self.messenger and get_uri(self.messenger.sender) 550 551 for attendee, attendee_attr in obj.get_uri_items("ATTENDEE") or []: 552 553 # Fix up the SENT-BY attribute for this user. 554 555 if attendee == self.user: 556 self.update_sender_attr(attendee_attr) 557 558 # Remove any conflicting SENT-BY attributes for other users. 559 560 elif attendee_attr.get("SENT-BY") == calendar_uri: 561 del attendee_attr["SENT-BY"] 562 563 def get_sending_attendee(self): 564 565 "Return the attendee who sent the current object." 566 567 # Search for the sender of the message or the calendar system address. 568 569 senders = set(uri_values(self.senders or self.messenger and [self.messenger.sender] or [])) 570 571 if senders: 572 573 # Obtain a mapping from sender URI to attendee URI, where the sender 574 # is taken from the SENT-BY attribute if present, or from the 575 # attendee value otherwise. 576 577 sent_by = get_sender_identities(self.obj.get_uri_map("ATTENDEE")) 578 579 # Obtain the attendee for the first sender matching the SENT-BY or 580 # attendee value. 581 582 for sender in senders.intersection(sent_by.keys()): 583 return sent_by[sender][0] 584 585 return None 586 587 # Object update methods. 588 589 def update_recurrenceid(self): 590 591 """ 592 Update the RECURRENCE-ID in the current object, initialising it from 593 DTSTART. 594 """ 595 596 self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")] 597 self.recurrenceid = self.obj.get_recurrenceid() 598 599 def update_dtstamp(self, obj=None): 600 601 "Update the DTSTAMP in the current object or any given object 'obj'." 602 603 obj = obj or self.obj 604 self.dtstamp = obj.update_dtstamp() 605 606 def update_sequence(self, obj=None): 607 608 "Update the SEQUENCE in the current object or any given object 'obj'." 609 610 obj = obj or self.obj 611 obj.update_sequence(self.is_organiser()) 612 613 def merge_attendance(self, attendees): 614 615 """ 616 Merge attendance from the current object's 'attendees' into the version 617 stored for the current user. 618 """ 619 620 obj = self.get_stored_object_version() 621 622 if not obj or not self.have_new_object(): 623 return False 624 625 # Get attendee details in a usable form. 626 627 stored_attendees = obj.get_uri_map("ATTENDEE") 628 629 for attendee, attendee_attr in attendees.items(): 630 631 # Update attendance in the loaded object for any recognised 632 # attendees. 633 634 if stored_attendees.has_key(attendee): 635 stored_attendees[attendee] = attendee_attr 636 637 update_attendees_with_delegates(stored_attendees, attendees) 638 639 # Set the new details and store the object. 640 641 obj["ATTENDEE"] = stored_attendees.items() 642 643 # Set a specific recurrence or the complete event if not an additional 644 # occurrence. 645 646 return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node()) 647 648 def update_attendees(self, to_invite, to_cancel, to_modify): 649 650 """ 651 Update the attendees in the current object with the given 'to_invite', 652 'to_cancel' and 'to_modify' attendee mappings. 653 """ 654 655 attendees = self.obj.get_uri_items("ATTENDEE") or [] 656 attendee_map = OrderedDict(attendees) 657 658 # Normalise the identities. 659 660 to_invite = uri_dict(to_invite) 661 to_cancel = uri_dict(to_cancel) 662 to_modify = uri_dict(to_modify) 663 664 if self.is_organiser(): 665 666 # Remove uninvited attendees. 667 668 for attendee in to_cancel.keys(): 669 if attendee_map.has_key(attendee): 670 del attendee_map[attendee] 671 672 # Attendees (when countering) must only include the current user and 673 # any added attendees. 674 675 else: 676 attr = attendee_map.get(self.user) or self.get_user_attributes() 677 attendee_map = {self.user : attr} 678 679 # Update modified attendees. 680 681 for attendee, attr in to_modify.items(): 682 existing_attr = attendee_map.get(attendee) 683 if existing_attr: 684 existing_attr.update(attr) 685 686 # Add newly-invited attendees, applicable for organisers and attendees 687 # (when countering). 688 689 for attendee, attr in to_invite.items(): 690 if not attendee_map.has_key(attendee): 691 692 # Only the organiser can reset the participation attributes. 693 694 if self.is_organiser() and attendee != self.user: 695 attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}) 696 697 attendee_map[attendee] = attr 698 699 self.obj["ATTENDEE"] = attendee_map.items() 700 701 def update_participation(self, partstat=None): 702 703 """ 704 Update the participation in the current object of the user with the 705 given 'partstat'. 706 """ 707 708 attendee_attr = self.obj.get_uri_map("ATTENDEE").get(self.user) 709 710 if not attendee_attr: 711 return None 712 713 # Set the participation and remove any request for response indicator. 714 715 if partstat: 716 attendee_attr["PARTSTAT"] = partstat 717 if attendee_attr.has_key("RSVP"): 718 del attendee_attr["RSVP"] 719 720 return attendee_attr 721 722 def update_event_version(self, changed=False): 723 724 """ 725 Update the event version information and details for sending. Where 726 'changed' is set to a true value for the attendee, the attendee list 727 will be processed and preserved; otherwise, the attendee replying will 728 be retained and the others removed from the object. 729 """ 730 731 if self.is_organiser(): 732 self.update_sender() 733 734 # Process attendee SENT-BY usage, timestamp and sequence details 735 # appropriately for the sender's role. 736 737 self.update_senders() 738 self.update_dtstamp() 739 self.update_sequence() 740 741 return True 742 743 def update_event_from_periods(self, to_set, to_exclude): 744 745 """ 746 Set the periods in any redefined event from the 'to_set' list, excluding 747 the main period if it appears in 'to_exclude'. 748 """ 749 750 if to_set: 751 self.obj.set_periods(to_set) 752 753 if to_exclude: 754 self.obj.update_exceptions(to_exclude, to_set or []) 755 756 # General message generation methods. 757 758 def get_recipients(self, obj=None): 759 760 """ 761 Return recipients for 'obj' (or the current object) dependent on the 762 current user's role. 763 """ 764 765 obj = obj or self.obj 766 767 organiser = obj.get_uri("ORGANIZER") 768 attendees = obj.get_uri_values("ATTENDEE") 769 770 # As organiser, send an invitation to attendees, excluding oneself if 771 # also attending. The updated event will be saved by the outgoing 772 # handler. 773 774 if self.is_organiser(): 775 if not attendees: 776 return [] 777 else: 778 l = [] 779 for attendee in attendees: 780 if attendee != self.user: 781 l.append(get_address(attendee)) 782 return l 783 else: 784 return [get_address(organiser)] 785 786 def attach_freebusy(self, parts): 787 788 """ 789 Since the outgoing handler updates this user's free/busy details, the 790 stored details will probably not have the updated details straight away, 791 so we update our copy for serialisation as the bundled free/busy object. 792 """ 793 794 freebusy = self.store.get_freebusy(self.user).copy() 795 self.update_freebusy(freebusy, self.user, self.is_organiser()) 796 797 # Bundle free/busy information if appropriate. 798 799 part = self.get_freebusy_part(freebusy) 800 if part: 801 parts.append(part) 802 803 def make_message(self, parts, recipients, bcc_sender=False): 804 805 """ 806 Send the given 'parts' to the appropriate 'recipients', also sending a 807 copy to the sender. 808 """ 809 810 if not self.messenger: 811 return None 812 813 # Update and attach bundled free/busy details. 814 815 self.attach_freebusy(parts) 816 817 if not bcc_sender: 818 return self.messenger.make_outgoing_message(parts, recipients) 819 else: 820 sender = get_address(self.user) 821 return self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) 822 823 def send_message(self, message, recipients, bcc_sender=False): 824 825 """ 826 Send 'message' to 'recipients', explicitly specifying the sender as an 827 outgoing BCC recipient if 'bcc_sender' is set, since the generic 828 calendar user will be the actual sender. 829 """ 830 831 if not recipients and not bcc_sender or not self.messenger: 832 return 833 834 if not bcc_sender: 835 self.messenger.sendmail(recipients, message.as_string()) 836 else: 837 sender = get_address(self.user) 838 self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) 839 840 def make_message_for_self(self, parts): 841 842 "Send 'message' to the current user." 843 844 if not self.messenger: 845 return None 846 847 sender = get_address(self.user) 848 return self.messenger.make_outgoing_message(parts, [sender]) 849 850 def send_message_to_self(self, message): 851 852 "Send 'message' to the current user." 853 854 if not self.messenger: 855 return 856 857 sender = get_address(self.user) 858 self.messenger.sendmail([sender], message.as_string()) 859 860 # Specific message generation methods. 861 862 def get_rescheduled_objects(self, periods, method): 863 864 "Return objects describing rescheduled 'periods' for 'method'." 865 866 objects = [] 867 868 if periods: 869 870 # For new recurrences, duplicate the core of the object without any 871 # period information. 872 873 template = self.obj.copy() 874 template.remove_all(["EXDATE", "RRULE", "RDATE", "DTSTART", "DTEND", 875 "DURATION"]) 876 877 # Process each period, attempting to update existing recurrences or 878 # creating new ones. 879 880 for p in periods: 881 if not p.origin: 882 continue 883 884 # Attempt to find an existing recurrence. 885 886 obj = self.get_stored_object(self.uid, p.get_recurrenceid()) 887 888 # Use the template without an existing recurrence. 889 890 if not obj: 891 obj = template.copy() 892 893 # Added methods do not employ a recurrence identifier. 894 895 if method != "ADD": 896 897 # Acquire the original recurrence identifier associated with 898 # this period. This may differ where the start of the period 899 # has changed. 900 901 dt, attr = p.get_recurrenceid_item() 902 obj["RECURRENCE-ID"] = [(format_datetime(dt), attr)] 903 904 # Update any sequence number if the period has changed from any 905 # stored version. 906 # NOTE: Attendance changes would also be propagated and have 907 # NOTE: this effect. 908 909 else: 910 main = obj.get_main_period() 911 if main != p: 912 self.update_sequence(obj) 913 914 # Set specific recurrence information. 915 916 obj.set_datetime("DTSTART", p.get_start()) 917 obj.set_datetime("DTEND", p.get_end()) 918 919 objects.append(obj) 920 921 return objects 922 923 def get_rescheduled_parts(self, periods, method): 924 925 """ 926 Return message parts describing rescheduled 'periods' affected by 'method'. 927 """ 928 929 rescheduled_parts = [] 930 931 for obj in self.get_rescheduled_objects(periods, method): 932 rescheduled_parts.append(self.object_to_part(method, obj)) 933 934 return rescheduled_parts 935 936 def make_update_message(self, recipients, update_parent=False, 937 to_unschedule=None, to_reschedule=None, 938 all_unscheduled=None, all_rescheduled=None, 939 to_add=None): 940 941 """ 942 Prepare event updates from the organiser of an event for the given 943 'recipients', including the parent event if 'update_parent' is set to a 944 true value. 945 946 Additional parts are provided by the 'to_unschedule' and 'to_reschedule' 947 collections. Alternatively, where the parent event is being updated, the 948 'all_unscheduled' and 'all_rescheduled' period collections are included. 949 950 The 'to_add' period collection augments the existing periods. 951 """ 952 953 parts = [] 954 955 if update_parent: 956 parts.append(self.object_to_part("REQUEST", self.obj)) 957 unscheduled = all_unscheduled 958 rescheduled = all_rescheduled 959 else: 960 unscheduled = to_unschedule 961 rescheduled = to_reschedule 962 963 parts += self.get_rescheduled_parts(unscheduled, "CANCEL") 964 parts += self.get_rescheduled_parts(rescheduled, "REQUEST") 965 parts += self.get_rescheduled_parts(to_add, "ADD") 966 return self.make_message(parts, recipients) 967 968 def make_self_update_message(self, all_unscheduled=None, all_rescheduled=None, 969 to_add=None): 970 971 """ 972 Prepare event updates to be sent from the organiser of an event to 973 themself. 974 """ 975 976 parts = [] 977 978 # Do not publish the event if it is a recurrence and is cancelled or 979 # rescheduled. 980 981 main = self.obj.get_recurrenceid() and self.obj.get_main_period() or None 982 983 if not main or (not main in all_unscheduled and not main in all_rescheduled): 984 parts += [self.object_to_part("PUBLISH", self.obj)] 985 986 parts += self.get_rescheduled_parts(all_unscheduled, "CANCEL") 987 parts += self.get_rescheduled_parts(all_rescheduled, "PUBLISH") 988 parts += self.get_rescheduled_parts(to_add, "ADD") 989 return self.make_message_for_self(parts) 990 991 def make_response_message(self, recipients, update_parent=False, 992 all_rescheduled=None, to_reschedule=None): 993 994 """ 995 Prepare a response to 'recipients', including the parent event if 996 'update_parent' is set to a true value, incorporating 'all_rescheduled' 997 periods, of which there may be indicated periods 'to_reschedule'. 998 """ 999 1000 parts = [self.object_to_part(update_parent and "COUNTER" or "REPLY", self.obj)] 1001 1002 # Determine existing replaced periods that are not newly rescheduled. 1003 1004 rescheduled_unmodified = set(all_rescheduled or []).difference(to_reschedule or []) 1005 1006 if rescheduled_unmodified: 1007 parts += self.get_rescheduled_parts(rescheduled_unmodified, update_parent and "COUNTER" or "REPLY") 1008 1009 # Suggest details for newly rescheduled periods. 1010 1011 if to_reschedule: 1012 parts += self.get_rescheduled_parts(to_reschedule, "COUNTER") 1013 1014 return self.make_message(parts, recipients, bcc_sender=True) 1015 1016 def make_cancel_message(self, to_cancel=None): 1017 1018 """ 1019 Prepare an event cancellation message involving the participants in the 1020 'to_cancel' mapping or involving all participants if 'to_cancel' is not 1021 specified or empty. 1022 """ 1023 1024 obj = self.obj.copy() 1025 1026 if to_cancel: 1027 obj["ATTENDEE"] = to_cancel.items() 1028 1029 recipients = self.get_recipients(obj) 1030 1031 parts = [self.object_to_part("CANCEL", obj)] 1032 return self.make_message(parts, recipients) 1033 1034 def make_cancel_message_for_self(self, obj): 1035 1036 "Prepare an event cancellation for the current user." 1037 1038 parts = [self.object_to_part("CANCEL", obj)] 1039 return self.make_message_for_self(parts) 1040 1041 # Action methods. 1042 1043 def send_declined_counter_to_attendee(self, attendee): 1044 1045 "Send a declined counter-proposal to 'attendee'." 1046 1047 # Obtain the counter-proposal for the attendee. 1048 1049 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 1050 if not obj: 1051 return 1052 1053 self.update_senders(obj) 1054 obj.update_dtstamp() 1055 obj.update_sequence() 1056 1057 parts = [self.object_to_part("DECLINECOUNTER", obj)] 1058 1059 # Create and send the response. 1060 1061 recipients = self.get_recipients(obj) 1062 message = self.make_message(parts, recipients, bcc_sender=True) 1063 self.send_message(message, recipients, bcc_sender=True) 1064 1065 def send_response_to_organiser(self, all_rescheduled=None, to_reschedule=None, 1066 changed=False): 1067 1068 """ 1069 Send a response to the organiser describing attendance and proposed 1070 amendments to the event. 1071 1072 If 'all_rescheduled' is specified, it provides details of separate 1073 recurrence instances for which a response needs to be generated. 1074 1075 If 'to_reschedule' provides rescheduled periods, these will be sent as 1076 counter-proposals. 1077 1078 If 'changed' is set to a true value, a counter-proposal will be sent for 1079 the entire event instead of a reply. 1080 """ 1081 1082 recipients = self.get_recipients() 1083 message = self.make_response_message(recipients, all_rescheduled, 1084 to_reschedule, changed) 1085 self.send_message(message, recipients, bcc_sender=True) 1086 1087 def send_update_to_recipients(self, to_unschedule=None, to_reschedule=None): 1088 1089 """ 1090 Send cancellations for each of the recurrences 'to_unschedule' along 1091 with modifications for each of the recurrences 'to_reschedule'. 1092 """ 1093 1094 recipients = self.get_recipients() 1095 message = self.make_update_message(recipients, to_unschedule, to_reschedule) 1096 self.send_message(message, recipients) 1097 1098 def send_publish_to_self(self, all_unscheduled=None, all_rescheduled=None): 1099 1100 """ 1101 Send published event details incorporating 'all_unscheduled' and 1102 'all_rescheduled' periods. 1103 """ 1104 1105 # Since the organiser can update the SEQUENCE but this can leave any 1106 # mail/calendar client lagging, issue a PUBLISH message to the 1107 # user's address. 1108 1109 recipients = self.get_recipients() 1110 message = self.make_self_update_message(all_unscheduled, all_rescheduled) 1111 self.send_message_to_self(message) 1112 1113 def send_cancel_to_recipients(self, to_cancel=None): 1114 1115 "Send a cancellation to all uninvited attendees in 'to_cancel'." 1116 1117 message = self.make_cancel_message(to_cancel) 1118 self.send_message(message, to_cancel.keys()) 1119 1120 def send_cancel_to_self(self): 1121 1122 "Issue a CANCEL message to the user's address." 1123 1124 message = self.make_cancel_message_for_self(self.obj) 1125 self.send_message_to_self(message) 1126 1127 # Object-related tests. 1128 1129 def is_recognised_organiser(self, organiser): 1130 1131 """ 1132 Return whether the given 'organiser' is recognised from 1133 previously-received details. If no stored details exist, True is 1134 returned. 1135 """ 1136 1137 obj = self.get_stored_object_version() 1138 if obj: 1139 stored_organiser = obj.get_uri("ORGANIZER") 1140 return stored_organiser == organiser 1141 else: 1142 return True 1143 1144 def is_recognised_attendee(self, attendee): 1145 1146 """ 1147 Return whether the given 'attendee' is recognised from 1148 previously-received details. If no stored details exist, True is 1149 returned. 1150 """ 1151 1152 obj = self.get_stored_object_version() 1153 if obj: 1154 stored_attendees = obj.get_uri_map("ATTENDEE") 1155 return stored_attendees.has_key(attendee) 1156 else: 1157 return True 1158 1159 def get_attendance(self, user=None, obj=None): 1160 1161 """ 1162 Return the attendance attributes for 'user', or the current user if 1163 'user' is not specified. 1164 """ 1165 1166 attendees = (obj or self.obj).get_uri_map("ATTENDEE") 1167 return attendees.get(user or self.user) 1168 1169 def is_participating(self, user, as_organiser=False, obj=None): 1170 1171 """ 1172 Return whether, subject to the 'user' indicating an identity and the 1173 'as_organiser' status of that identity, the user concerned is actually 1174 participating in the current object event. 1175 """ 1176 1177 # Use any attendee property information for an organiser, not the 1178 # organiser property attributes. 1179 1180 attr = self.get_attendance(user, obj) 1181 return as_organiser or attr is not None and not attr or \ 1182 attr and attr.get("PARTSTAT") not in ("DECLINED", "DELEGATED", "NEEDS-ACTION") 1183 1184 def has_indicated_attendance(self, user=None, obj=None): 1185 1186 """ 1187 Return whether the given 'user' (or the current user if not specified) 1188 has indicated attendance in the given 'obj' (or the current object if 1189 not specified). 1190 """ 1191 1192 attr = self.get_attendance(user, obj) 1193 return attr and attr.get("PARTSTAT") not in (None, "NEEDS-ACTION") 1194 1195 def get_overriding_transparency(self, user, as_organiser=False): 1196 1197 """ 1198 Return the overriding transparency to be associated with the free/busy 1199 records for an event, subject to the 'user' indicating an identity and 1200 the 'as_organiser' status of that identity. 1201 1202 Where an identity is only an organiser and not attending, "ORG" is 1203 returned. Otherwise, no overriding transparency is defined and None is 1204 returned. 1205 """ 1206 1207 attr = self.get_attendance(user) 1208 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 1209 1210 def can_schedule(self, freebusy, periods): 1211 1212 """ 1213 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 1214 """ 1215 1216 return freebusy.can_schedule(periods, self.uid, self.recurrenceid) 1217 1218 def have_new_object(self, strict=True): 1219 1220 """ 1221 Return whether the current object is new to the current user. 1222 1223 If 'strict' is specified and is a false value, the DTSTAMP test will be 1224 ignored. This is useful in handling responses from attendees from 1225 clients (like Claws Mail) that erase time information from DTSTAMP and 1226 make it invalid. 1227 """ 1228 1229 obj = self.get_stored_object_version() 1230 1231 # If found, compare SEQUENCE and potentially DTSTAMP. 1232 1233 if obj: 1234 sequence = obj.get_value("SEQUENCE") 1235 dtstamp = obj.get_value("DTSTAMP") 1236 1237 # If the request refers to an older version of the object, ignore 1238 # it. 1239 1240 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 1241 1242 return True 1243 1244 def possibly_recurring_indefinitely(self): 1245 1246 "Return whether the object recurs indefinitely." 1247 1248 # Obtain the stored object to make sure that recurrence information 1249 # is not being ignored. This might happen if a client sends a 1250 # cancellation without the complete set of properties, for instance. 1251 1252 return self.obj.possibly_recurring_indefinitely() or \ 1253 self.get_stored_object_version() and \ 1254 self.get_stored_object_version().possibly_recurring_indefinitely() 1255 1256 # Constraint application on event periods. 1257 1258 def check_object(self): 1259 1260 "Check the object against any scheduling constraints." 1261 1262 permitted_values = self.get_permitted_values() 1263 if not permitted_values: 1264 return None 1265 1266 invalid = [] 1267 1268 for period in self.obj.get_periods(): 1269 errors = period.check_permitted(permitted_values) 1270 if errors: 1271 start_errors, end_errors = errors 1272 invalid.append((period.origin, start_errors, end_errors)) 1273 1274 return invalid 1275 1276 def correct_object(self): 1277 1278 "Correct the object according to any scheduling constraints." 1279 1280 permitted_values = self.get_permitted_values() 1281 return permitted_values and self.obj.correct_object(permitted_values) 1282 1283 def correct_period(self, period): 1284 1285 "Correct 'period' according to any scheduling constraints." 1286 1287 permitted_values = self.get_permitted_values() 1288 if not permitted_values: 1289 return period 1290 else: 1291 return period.get_corrected(permitted_values) 1292 1293 # Object retrieval. 1294 1295 def get_stored_object_version(self): 1296 1297 """ 1298 Return the stored object to which the current object refers for the 1299 current user. 1300 """ 1301 1302 return self.get_stored_object(self.uid, self.recurrenceid) 1303 1304 def get_definitive_object(self, as_organiser): 1305 1306 """ 1307 Return an object considered definitive for the current transaction, 1308 using 'as_organiser' to select the current transaction's object if 1309 false, or selecting a stored object if true. 1310 """ 1311 1312 return not as_organiser and self.obj or self.get_stored_object_version() 1313 1314 def get_parent_object(self): 1315 1316 """ 1317 Return the parent object to which the current object refers for the 1318 current user. 1319 """ 1320 1321 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 1322 1323 # Convenience methods for modifying free/busy collections. 1324 1325 def get_recurrence_start_point(self, recurrenceid): 1326 1327 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 1328 1329 return self.obj.get_recurrence_start_point(recurrenceid) 1330 1331 def remove_from_freebusy(self, freebusy, participant=None): 1332 1333 """ 1334 Remove this event from the given 'freebusy' collection. If 'participant' 1335 is specified, only remove this event if the participant is attending. 1336 """ 1337 1338 removed = freebusy.remove_event_periods(self.uid, self.recurrenceid, participant) 1339 if not removed and self.recurrenceid: 1340 return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid), participant) 1341 else: 1342 return removed 1343 1344 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 1345 1346 """ 1347 Remove from 'freebusy' any original recurrence from parent free/busy 1348 details for the current object, if the current object is a specific 1349 additional recurrence. Otherwise, remove all additional recurrence 1350 information corresponding to 'recurrenceids', or if omitted, all 1351 recurrences. 1352 """ 1353 1354 if self.recurrenceid: 1355 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 1356 freebusy.remove_affected_period(self.uid, recurrenceid) 1357 else: 1358 # Remove obsolete recurrence periods. 1359 1360 freebusy.remove_additional_periods(self.uid, recurrenceids) 1361 1362 # Remove original periods affected by additional recurrences. 1363 1364 if recurrenceids: 1365 for recurrenceid in recurrenceids: 1366 recurrenceid = self.get_recurrence_start_point(recurrenceid) 1367 freebusy.remove_affected_period(self.uid, recurrenceid) 1368 1369 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 1370 1371 """ 1372 Update the 'freebusy' collection for this event with the periods and 1373 transparency associated with the current object, subject to the 'user' 1374 identity and the attendance details provided for them, indicating 1375 whether the update is being done 'as_organiser' (for the organiser of 1376 an event) or not. 1377 1378 If 'offer' is set to a true value, any free/busy updates will be tagged 1379 with an expiry time. 1380 """ 1381 1382 # Obtain the stored object if the current object is not issued by the 1383 # organiser. Attendees do not have the opportunity to redefine the 1384 # periods. 1385 1386 obj = self.get_definitive_object(as_organiser) 1387 if not obj: 1388 return 1389 1390 # Obtain the affected periods. 1391 1392 periods = self.get_periods(obj, future_only=True) 1393 1394 # Define an overriding transparency, the indicated event transparency, 1395 # or the default transparency for the free/busy entry. 1396 1397 transp = self.get_overriding_transparency(user, as_organiser) or \ 1398 obj.get_value("TRANSP") or \ 1399 "OPAQUE" 1400 1401 # Calculate any expiry time. If no offer period is defined, do not 1402 # record the offer periods. 1403 1404 if offer: 1405 offer_period = self.get_offer_period() 1406 if offer_period: 1407 expires = get_timestamp(offer_period) 1408 else: 1409 return 1410 else: 1411 expires = None 1412 1413 # Perform the low-level update. 1414 1415 Client.update_freebusy(self, freebusy, periods, transp, 1416 self.uid, self.recurrenceid, 1417 obj.get_value("SUMMARY"), 1418 obj.get_uri("ORGANIZER"), 1419 expires) 1420 1421 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 1422 updating_other=False, offer=False): 1423 1424 """ 1425 Update the 'freebusy' collection for the given 'user', indicating 1426 whether the update is 'for_organiser' (being done for the organiser of 1427 an event) or not, and whether it is 'updating_other' (meaning another 1428 user's details). 1429 1430 If 'offer' is set to a true value, any free/busy updates will be tagged 1431 with an expiry time. 1432 """ 1433 1434 # Record in the free/busy details unless a non-participating attendee. 1435 # Remove periods for non-participating attendees. 1436 1437 if offer or self.is_participating(user, for_organiser and not updating_other): 1438 self.update_freebusy(freebusy, user, 1439 for_organiser and not updating_other or 1440 not for_organiser and updating_other, 1441 offer 1442 ) 1443 else: 1444 self.remove_from_freebusy(freebusy) 1445 1446 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 1447 updating_other=False): 1448 1449 """ 1450 Remove details from the 'freebusy' collection for the given 'user', 1451 indicating whether the modification is 'for_organiser' (being done for 1452 the organiser of an event) or not, and whether it is 'updating_other' 1453 (meaning another user's details). 1454 """ 1455 1456 # Remove from the free/busy details if a specified attendee. 1457 1458 if self.is_participating(user, for_organiser and not updating_other): 1459 self.remove_from_freebusy(freebusy) 1460 1461 # Convenience methods for updating stored free/busy information received 1462 # from other users. 1463 1464 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 1465 1466 """ 1467 For the current user, record the free/busy information for another 1468 'user', indicating whether the update is 'for_organiser' or not, thus 1469 maintaining a separate record of their free/busy details. 1470 """ 1471 1472 fn = fn or self.update_freebusy_for_participant 1473 1474 # A user does not store free/busy information for themself as another 1475 # party. 1476 1477 if user == self.user: 1478 return 1479 1480 self.acquire_lock() 1481 try: 1482 freebusy = self.store.get_freebusy_for_other_for_update(self.user, user) 1483 fn(freebusy, user, for_organiser, True) 1484 1485 # Tidy up any obsolete recurrences. 1486 1487 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1488 self.store.set_freebusy_for_other(self.user, freebusy, user) 1489 1490 finally: 1491 self.release_lock() 1492 1493 def update_freebusy_from_organiser(self, organiser): 1494 1495 "For the current user, record free/busy information from 'organiser'." 1496 1497 self.update_freebusy_from_participant(organiser, True) 1498 1499 def update_freebusy_from_attendees(self, attendees): 1500 1501 "For the current user, record free/busy information from 'attendees'." 1502 1503 obj = self.get_stored_object_version() 1504 1505 if not obj or not self.have_new_object(): 1506 return False 1507 1508 # Filter out unrecognised attendees. 1509 1510 attendees = set(attendees).intersection(obj.get_uri_values("ATTENDEE")) 1511 1512 for attendee in attendees: 1513 self.update_freebusy_from_participant(attendee, False) 1514 1515 return True 1516 1517 def remove_freebusy_from_organiser(self, organiser): 1518 1519 "For the current user, remove free/busy information from 'organiser'." 1520 1521 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 1522 1523 def remove_freebusy_from_attendees(self, attendees): 1524 1525 "For the current user, remove free/busy information from 'attendees'." 1526 1527 for attendee in attendees.keys(): 1528 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 1529 1530 # Convenience methods for updating free/busy details at the event level. 1531 1532 def update_event_in_freebusy(self, for_organiser=True): 1533 1534 """ 1535 Update free/busy information when handling an object, doing so for the 1536 organiser of an event if 'for_organiser' is set to a true value. 1537 """ 1538 1539 freebusy = self.store.get_freebusy_for_update(self.user) 1540 1541 # Obtain the attendance attributes for this user, if available. 1542 1543 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 1544 1545 # Remove original recurrence details replaced by additional 1546 # recurrences, as well as obsolete additional recurrences. 1547 1548 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1549 self.store.set_freebusy(self.user, freebusy) 1550 1551 if self.publisher and self.is_sharing() and self.is_publishing(): 1552 self.publisher.set_freebusy(self.user, freebusy) 1553 1554 # Update free/busy provider information if the event may recur 1555 # indefinitely. 1556 1557 if self.possibly_recurring_indefinitely(): 1558 self.store.append_freebusy_provider(self.user, self.obj) 1559 1560 return True 1561 1562 def remove_event_from_freebusy(self): 1563 1564 "Remove free/busy information when handling an object." 1565 1566 freebusy = self.store.get_freebusy_for_update(self.user) 1567 1568 self.remove_from_freebusy(freebusy) 1569 self.remove_freebusy_for_recurrences(freebusy) 1570 self.store.set_freebusy(self.user, freebusy) 1571 1572 if self.publisher and self.is_sharing() and self.is_publishing(): 1573 self.publisher.set_freebusy(self.user, freebusy) 1574 1575 # Update free/busy provider information if the event may recur 1576 # indefinitely. 1577 1578 if self.possibly_recurring_indefinitely(): 1579 self.store.remove_freebusy_provider(self.user, self.obj) 1580 1581 def update_event_in_freebusy_offers(self): 1582 1583 "Update free/busy offers when handling an object." 1584 1585 freebusy = self.store.get_freebusy_offers_for_update(self.user) 1586 1587 # Obtain the attendance attributes for this user, if available. 1588 1589 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 1590 1591 # Remove original recurrence details replaced by additional 1592 # recurrences, as well as obsolete additional recurrences. 1593 1594 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1595 self.store.set_freebusy_offers(self.user, freebusy) 1596 1597 return True 1598 1599 def remove_event_from_freebusy_offers(self): 1600 1601 "Remove free/busy offers when handling an object." 1602 1603 freebusy = self.store.get_freebusy_offers_for_update(self.user) 1604 1605 self.remove_from_freebusy(freebusy) 1606 self.remove_freebusy_for_recurrences(freebusy) 1607 self.store.set_freebusy_offers(self.user, freebusy) 1608 1609 return True 1610 1611 # Convenience methods for removing counter-proposals and updating the 1612 # request queue. 1613 1614 def remove_request(self): 1615 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1616 1617 def remove_event(self): 1618 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 1619 1620 def remove_counter(self, attendee): 1621 self.remove_counters([attendee]) 1622 1623 def remove_counters(self, attendees): 1624 for attendee in attendees: 1625 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 1626 1627 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 1628 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1629 1630 # vim: tabstop=4 expandtab shiftwidth=4