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