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