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