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() and attendee != self.user: 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 if to_exclude: 808 self.obj.update_exceptions(to_exclude, to_set or []) 809 810 # General message generation methods. 811 812 def get_recipients(self, obj=None): 813 814 """ 815 Return recipients for 'obj' (or the current object) dependent on the 816 current user's role. 817 """ 818 819 obj = obj or self.obj 820 821 organiser = get_uri(obj.get_value("ORGANIZER")) 822 attendees = uri_values(obj.get_values("ATTENDEE")) 823 824 # As organiser, send an invitation to attendees, excluding oneself if 825 # also attending. The updated event will be saved by the outgoing 826 # handler. 827 828 if self.is_organiser(): 829 return [get_address(attendee) for attendee in attendees if attendee != self.user] 830 else: 831 return [get_address(organiser)] 832 833 def attach_freebusy(self, parts): 834 835 """ 836 Since the outgoing handler updates this user's free/busy details, the 837 stored details will probably not have the updated details straight away, 838 so we update our copy for serialisation as the bundled free/busy object. 839 """ 840 841 freebusy = self.store.get_freebusy(self.user).copy() 842 self.update_freebusy(freebusy, self.user, self.is_organiser()) 843 844 # Bundle free/busy information if appropriate. 845 846 part = self.get_freebusy_part(freebusy) 847 if part: 848 parts.append(part) 849 850 def make_message(self, parts, recipients, bcc_sender=False): 851 852 """ 853 Send the given 'parts' to the appropriate 'recipients', also sending a 854 copy to the sender. 855 """ 856 857 if not self.messenger: 858 return None 859 860 # Update and attach bundled free/busy details. 861 862 self.attach_freebusy(parts) 863 864 if not bcc_sender: 865 return self.messenger.make_outgoing_message(parts, recipients) 866 else: 867 sender = get_address(self.user) 868 return self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) 869 870 def send_message(self, message, recipients, bcc_sender=False): 871 872 """ 873 Send 'message' to 'recipients', explicitly specifying the sender as an 874 outgoing BCC recipient if 'bcc_sender' is set, since the generic 875 calendar user will be the actual sender. 876 """ 877 878 if not recipients and not bcc_sender or not self.messenger: 879 return 880 881 if not bcc_sender: 882 self.messenger.sendmail(recipients, message.as_string()) 883 else: 884 sender = get_address(self.user) 885 self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) 886 887 def make_message_for_self(self, parts): 888 889 "Send 'message' to the current user." 890 891 if not self.messenger: 892 return None 893 894 sender = get_address(self.user) 895 return self.messenger.make_outgoing_message(parts, [sender]) 896 897 def send_message_to_self(self, message): 898 899 "Send 'message' to the current user." 900 901 if not self.messenger: 902 return 903 904 sender = get_address(self.user) 905 self.messenger.sendmail([sender], message.as_string()) 906 907 # Specific message generation methods. 908 909 def get_rescheduled_parts(self, periods, method): 910 911 """ 912 Return message parts describing rescheduled 'periods' affected by 'method'. 913 """ 914 915 rescheduled_parts = [] 916 917 if periods: 918 919 # Duplicate the core of the object without any period information. 920 921 obj = self.obj.copy() 922 obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) 923 924 for p in periods: 925 if not p.origin: 926 continue 927 928 # Set specific recurrence information. 929 930 obj.set_datetime("DTSTART", p.get_start()) 931 obj.set_datetime("DTEND", p.get_end()) 932 933 # Acquire the original recurrence identifier associated with 934 # this period. This may differ where the start of the period has 935 # changed. 936 937 dt, attr = p.get_recurrenceid_item() 938 obj["RECURRENCE-ID"] = [(format_datetime(dt), attr)] 939 940 rescheduled_parts.append(self.object_to_part(method, obj)) 941 942 return rescheduled_parts 943 944 def make_update_message(self, recipients, update_parent=False, 945 to_unschedule=None, to_reschedule=None, 946 all_unscheduled=None, all_rescheduled=None, 947 to_add=None): 948 949 """ 950 Prepare event updates from the organiser of an event for the given 951 'recipients', including the parent event if 'update_parent' is set to a 952 true value. 953 954 Additional parts are provided by the 'to_unschedule' and 'to_reschedule' 955 collections. Alternatively, where the parent event is being updated, the 956 'all_unscheduled' and 'all_rescheduled' period collections are included. 957 958 The 'to_add' period collection augments the existing periods. 959 """ 960 961 parts = [] 962 963 if update_parent: 964 parts.append(self.object_to_part("REQUEST", self.obj)) 965 unscheduled = all_unscheduled 966 rescheduled = all_rescheduled 967 else: 968 unscheduled = to_unschedule 969 rescheduled = to_reschedule 970 971 parts += self.get_rescheduled_parts(unscheduled, "CANCEL") 972 parts += self.get_rescheduled_parts(rescheduled, "REQUEST") 973 parts += self.get_rescheduled_parts(to_add, "ADD") 974 return self.make_message(parts, recipients) 975 976 def make_self_update_message(self, all_unscheduled=None, all_rescheduled=None, 977 to_add=None): 978 979 """ 980 Prepare event updates to be sent from the organiser of an event to 981 themself. 982 """ 983 984 parts = [self.object_to_part("PUBLISH", self.obj)] 985 parts += self.get_rescheduled_parts(all_unscheduled, "CANCEL") 986 parts += self.get_rescheduled_parts(all_rescheduled, "PUBLISH") 987 parts += self.get_rescheduled_parts(to_add, "ADD") 988 return self.make_message_for_self(parts) 989 990 def make_response_message(self, recipients, update_parent=False, 991 all_rescheduled=None, to_reschedule=None): 992 993 """ 994 Prepare a response to 'recipients', including the parent event if 995 'update_parent' is set to a true value, incorporating 'all_rescheduled' 996 periods, of which there may be indicated periods 'to_reschedule'. 997 """ 998 999 parts = [self.object_to_part(update_parent and "COUNTER" or "REPLY", self.obj)] 1000 1001 # Determine existing replaced periods that are not newly rescheduled. 1002 1003 rescheduled_unmodified = set(all_rescheduled or []).difference(to_reschedule or []) 1004 1005 if rescheduled_unmodified: 1006 parts += self.get_rescheduled_parts(rescheduled_unmodified, update_parent and "COUNTER" or "REPLY") 1007 1008 # Suggest details for newly rescheduled periods. 1009 1010 if to_reschedule: 1011 parts += self.get_rescheduled_parts(to_reschedule, "COUNTER") 1012 1013 return self.make_message(parts, recipients, bcc_sender=True) 1014 1015 def make_cancel_message(self, to_cancel=None): 1016 1017 """ 1018 Prepare an event cancellation message involving the participants in the 1019 'to_cancel' mapping. 1020 """ 1021 1022 if not to_cancel: 1023 return None 1024 1025 obj = self.obj.copy() 1026 obj["ATTENDEE"] = to_cancel.items() 1027 1028 parts = [self.object_to_part("CANCEL", obj)] 1029 return self.make_message(parts, to_cancel.keys()) 1030 1031 def make_cancel_message_for_self(self, obj): 1032 1033 "Prepare an event cancellation for the current user." 1034 1035 parts = [self.object_to_part("CANCEL", obj)] 1036 return self.make_message_for_self(parts) 1037 1038 # Action methods. 1039 1040 def send_declined_counter_to_attendee(self, attendee): 1041 1042 "Send a declined counter-proposal to 'attendee'." 1043 1044 # Obtain the counter-proposal for the attendee. 1045 1046 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 1047 if not obj: 1048 return 1049 1050 self.update_senders(obj) 1051 obj.update_dtstamp() 1052 obj.update_sequence() 1053 1054 parts = [self.object_to_part("DECLINECOUNTER", obj)] 1055 1056 # Create and send the response. 1057 1058 recipients = self.get_recipients(obj) 1059 message = self.make_message(parts, recipients, bcc_sender=True) 1060 self.send_message(message, recipients, bcc_sender=True) 1061 1062 def send_response_to_organiser(self, all_rescheduled=None, to_reschedule=None, 1063 changed=False): 1064 1065 """ 1066 Send a response to the organiser describing attendance and proposed 1067 amendments to the event. 1068 1069 If 'all_rescheduled' is specified, it provides details of separate 1070 recurrence instances for which a response needs to be generated. 1071 1072 If 'to_reschedule' provides rescheduled periods, these will be sent as 1073 counter-proposals. 1074 1075 If 'changed' is set to a true value, a counter-proposal will be sent for 1076 the entire event instead of a reply. 1077 """ 1078 1079 recipients = self.get_recipients() 1080 message = self.make_response_message(recipients, all_rescheduled, 1081 to_reschedule, changed) 1082 self.send_message(message, recipients, bcc_sender=True) 1083 1084 def send_update_to_recipients(self, to_unschedule=None, to_reschedule=None): 1085 1086 """ 1087 Send cancellations for each of the recurrences 'to_unschedule' along 1088 with modifications for each of the recurrences 'to_reschedule'. 1089 """ 1090 1091 recipients = self.get_recipients() 1092 message = self.make_update_message(recipients, to_unschedule, to_reschedule) 1093 self.send_message(message, recipients) 1094 1095 def send_publish_to_self(self, all_unscheduled=None, all_rescheduled=None): 1096 1097 """ 1098 Send published event details incorporating 'all_unscheduled' and 1099 'all_rescheduled' periods. 1100 """ 1101 1102 # Since the organiser can update the SEQUENCE but this can leave any 1103 # mail/calendar client lagging, issue a PUBLISH message to the 1104 # user's address. 1105 1106 recipients = self.get_recipients() 1107 message = self.make_self_update_message(all_unscheduled, all_rescheduled) 1108 self.send_message_to_self(message) 1109 1110 def send_cancel_to_recipients(self, to_cancel=None): 1111 1112 "Send a cancellation to all uninvited attendees in 'to_cancel'." 1113 1114 message = self.make_cancel_message(to_cancel) 1115 self.send_message(message, to_cancel.keys()) 1116 1117 def send_cancel_to_self(self): 1118 1119 "Issue a CANCEL message to the user's address." 1120 1121 message = self.make_cancel_message_for_self(self.obj) 1122 self.send_message_to_self(message) 1123 1124 # Object-related tests. 1125 1126 def is_recognised_organiser(self, organiser): 1127 1128 """ 1129 Return whether the given 'organiser' is recognised from 1130 previously-received details. If no stored details exist, True is 1131 returned. 1132 """ 1133 1134 obj = self.get_stored_object_version() 1135 if obj: 1136 stored_organiser = get_uri(obj.get_value("ORGANIZER")) 1137 return stored_organiser == organiser 1138 else: 1139 return True 1140 1141 def is_recognised_attendee(self, attendee): 1142 1143 """ 1144 Return whether the given 'attendee' is recognised from 1145 previously-received details. If no stored details exist, True is 1146 returned. 1147 """ 1148 1149 obj = self.get_stored_object_version() 1150 if obj: 1151 stored_attendees = uri_dict(obj.get_value_map("ATTENDEE")) 1152 return stored_attendees.has_key(attendee) 1153 else: 1154 return True 1155 1156 def get_attendance(self, user=None, obj=None): 1157 1158 """ 1159 Return the attendance attributes for 'user', or the current user if 1160 'user' is not specified. 1161 """ 1162 1163 attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE")) 1164 return attendees.get(user or self.user) 1165 1166 def is_participating(self, user, as_organiser=False, obj=None): 1167 1168 """ 1169 Return whether, subject to the 'user' indicating an identity and the 1170 'as_organiser' status of that identity, the user concerned is actually 1171 participating in the current object event. 1172 """ 1173 1174 # Use any attendee property information for an organiser, not the 1175 # organiser property attributes. 1176 1177 attr = self.get_attendance(user, obj) 1178 return as_organiser or attr is not None and not attr or \ 1179 attr and attr.get("PARTSTAT") not in ("DECLINED", "DELEGATED", "NEEDS-ACTION") 1180 1181 def has_indicated_attendance(self, user=None, obj=None): 1182 1183 """ 1184 Return whether the given 'user' (or the current user if not specified) 1185 has indicated attendance in the given 'obj' (or the current object if 1186 not specified). 1187 """ 1188 1189 attr = self.get_attendance(user, obj) 1190 return attr and attr.get("PARTSTAT") not in (None, "NEEDS-ACTION") 1191 1192 def get_overriding_transparency(self, user, as_organiser=False): 1193 1194 """ 1195 Return the overriding transparency to be associated with the free/busy 1196 records for an event, subject to the 'user' indicating an identity and 1197 the 'as_organiser' status of that identity. 1198 1199 Where an identity is only an organiser and not attending, "ORG" is 1200 returned. Otherwise, no overriding transparency is defined and None is 1201 returned. 1202 """ 1203 1204 attr = self.get_attendance(user) 1205 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 1206 1207 def can_schedule(self, freebusy, periods): 1208 1209 """ 1210 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 1211 """ 1212 1213 return freebusy.can_schedule(periods, self.uid, self.recurrenceid) 1214 1215 def have_new_object(self, strict=True): 1216 1217 """ 1218 Return whether the current object is new to the current user. 1219 1220 If 'strict' is specified and is a false value, the DTSTAMP test will be 1221 ignored. This is useful in handling responses from attendees from 1222 clients (like Claws Mail) that erase time information from DTSTAMP and 1223 make it invalid. 1224 """ 1225 1226 obj = self.get_stored_object_version() 1227 1228 # If found, compare SEQUENCE and potentially DTSTAMP. 1229 1230 if obj: 1231 sequence = obj.get_value("SEQUENCE") 1232 dtstamp = obj.get_value("DTSTAMP") 1233 1234 # If the request refers to an older version of the object, ignore 1235 # it. 1236 1237 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 1238 1239 return True 1240 1241 def possibly_recurring_indefinitely(self): 1242 1243 "Return whether the object recurs indefinitely." 1244 1245 # Obtain the stored object to make sure that recurrence information 1246 # is not being ignored. This might happen if a client sends a 1247 # cancellation without the complete set of properties, for instance. 1248 1249 return self.obj.possibly_recurring_indefinitely() or \ 1250 self.get_stored_object_version() and \ 1251 self.get_stored_object_version().possibly_recurring_indefinitely() 1252 1253 # Constraint application on event periods. 1254 1255 def check_object(self): 1256 1257 "Check the object against any scheduling constraints." 1258 1259 permitted_values = self.get_permitted_values() 1260 if not permitted_values: 1261 return None 1262 1263 invalid = [] 1264 1265 for period in self.obj.get_periods(self.get_tzid()): 1266 errors = period.check_permitted(permitted_values) 1267 if errors: 1268 start_errors, end_errors = errors 1269 invalid.append((period.origin, start_errors, end_errors)) 1270 1271 return invalid 1272 1273 def correct_object(self): 1274 1275 "Correct the object according to any scheduling constraints." 1276 1277 permitted_values = self.get_permitted_values() 1278 return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) 1279 1280 def correct_period(self, period): 1281 1282 "Correct 'period' according to any scheduling constraints." 1283 1284 permitted_values = self.get_permitted_values() 1285 if not permitted_values: 1286 return period 1287 else: 1288 return period.get_corrected(permitted_values) 1289 1290 # Object retrieval. 1291 1292 def get_stored_object_version(self): 1293 1294 """ 1295 Return the stored object to which the current object refers for the 1296 current user. 1297 """ 1298 1299 return self.get_stored_object(self.uid, self.recurrenceid) 1300 1301 def get_definitive_object(self, as_organiser): 1302 1303 """ 1304 Return an object considered definitive for the current transaction, 1305 using 'as_organiser' to select the current transaction's object if 1306 false, or selecting a stored object if true. 1307 """ 1308 1309 return not as_organiser and self.obj or self.get_stored_object_version() 1310 1311 def get_parent_object(self): 1312 1313 """ 1314 Return the parent object to which the current object refers for the 1315 current user. 1316 """ 1317 1318 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 1319 1320 # Convenience methods for modifying free/busy collections. 1321 1322 def get_recurrence_start_point(self, recurrenceid): 1323 1324 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 1325 1326 return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) 1327 1328 def remove_from_freebusy(self, freebusy, participant=None): 1329 1330 """ 1331 Remove this event from the given 'freebusy' collection. If 'participant' 1332 is specified, only remove this event if the participant is attending. 1333 """ 1334 1335 removed = freebusy.remove_event_periods(self.uid, self.recurrenceid, participant) 1336 if not removed and self.recurrenceid: 1337 return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid), participant) 1338 else: 1339 return removed 1340 1341 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 1342 1343 """ 1344 Remove from 'freebusy' any original recurrence from parent free/busy 1345 details for the current object, if the current object is a specific 1346 additional recurrence. Otherwise, remove all additional recurrence 1347 information corresponding to 'recurrenceids', or if omitted, all 1348 recurrences. 1349 """ 1350 1351 if self.recurrenceid: 1352 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 1353 freebusy.remove_affected_period(self.uid, recurrenceid) 1354 else: 1355 # Remove obsolete recurrence periods. 1356 1357 freebusy.remove_additional_periods(self.uid, recurrenceids) 1358 1359 # Remove original periods affected by additional recurrences. 1360 1361 if recurrenceids: 1362 for recurrenceid in recurrenceids: 1363 recurrenceid = self.get_recurrence_start_point(recurrenceid) 1364 freebusy.remove_affected_period(self.uid, recurrenceid) 1365 1366 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 1367 1368 """ 1369 Update the 'freebusy' collection for this event with the periods and 1370 transparency associated with the current object, subject to the 'user' 1371 identity and the attendance details provided for them, indicating 1372 whether the update is being done 'as_organiser' (for the organiser of 1373 an event) or not. 1374 1375 If 'offer' is set to a true value, any free/busy updates will be tagged 1376 with an expiry time. 1377 """ 1378 1379 # Obtain the stored object if the current object is not issued by the 1380 # organiser. Attendees do not have the opportunity to redefine the 1381 # periods. 1382 1383 obj = self.get_definitive_object(as_organiser) 1384 if not obj: 1385 return 1386 1387 # Obtain the affected periods. 1388 1389 periods = self.get_periods(obj, future_only=True) 1390 1391 # Define an overriding transparency, the indicated event transparency, 1392 # or the default transparency for the free/busy entry. 1393 1394 transp = self.get_overriding_transparency(user, as_organiser) or \ 1395 obj.get_value("TRANSP") or \ 1396 "OPAQUE" 1397 1398 # Calculate any expiry time. If no offer period is defined, do not 1399 # record the offer periods. 1400 1401 if offer: 1402 offer_period = self.get_offer_period() 1403 if offer_period: 1404 expires = get_timestamp(offer_period) 1405 else: 1406 return 1407 else: 1408 expires = None 1409 1410 # Perform the low-level update. 1411 1412 Client.update_freebusy(self, freebusy, periods, transp, 1413 self.uid, self.recurrenceid, 1414 obj.get_value("SUMMARY"), 1415 get_uri(obj.get_value("ORGANIZER")), 1416 expires) 1417 1418 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 1419 updating_other=False, offer=False): 1420 1421 """ 1422 Update the 'freebusy' collection for the given 'user', indicating 1423 whether the update is 'for_organiser' (being done for the organiser of 1424 an event) or not, and whether it is 'updating_other' (meaning another 1425 user's details). 1426 1427 If 'offer' is set to a true value, any free/busy updates will be tagged 1428 with an expiry time. 1429 """ 1430 1431 # Record in the free/busy details unless a non-participating attendee. 1432 # Remove periods for non-participating attendees. 1433 1434 if offer or self.is_participating(user, for_organiser and not updating_other): 1435 self.update_freebusy(freebusy, user, 1436 for_organiser and not updating_other or 1437 not for_organiser and updating_other, 1438 offer 1439 ) 1440 else: 1441 self.remove_from_freebusy(freebusy) 1442 1443 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 1444 updating_other=False): 1445 1446 """ 1447 Remove details from the 'freebusy' collection for the given 'user', 1448 indicating whether the modification is 'for_organiser' (being done for 1449 the organiser of an event) or not, and whether it is 'updating_other' 1450 (meaning another user's details). 1451 """ 1452 1453 # Remove from the free/busy details if a specified attendee. 1454 1455 if self.is_participating(user, for_organiser and not updating_other): 1456 self.remove_from_freebusy(freebusy) 1457 1458 # Convenience methods for updating stored free/busy information received 1459 # from other users. 1460 1461 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 1462 1463 """ 1464 For the current user, record the free/busy information for another 1465 'user', indicating whether the update is 'for_organiser' or not, thus 1466 maintaining a separate record of their free/busy details. 1467 """ 1468 1469 fn = fn or self.update_freebusy_for_participant 1470 1471 # A user does not store free/busy information for themself as another 1472 # party. 1473 1474 if user == self.user: 1475 return 1476 1477 self.acquire_lock() 1478 try: 1479 freebusy = self.store.get_freebusy_for_other_for_update(self.user, user) 1480 fn(freebusy, user, for_organiser, True) 1481 1482 # Tidy up any obsolete recurrences. 1483 1484 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1485 self.store.set_freebusy_for_other(self.user, freebusy, user) 1486 1487 finally: 1488 self.release_lock() 1489 1490 def update_freebusy_from_organiser(self, organiser): 1491 1492 "For the current user, record free/busy information from 'organiser'." 1493 1494 self.update_freebusy_from_participant(organiser, True) 1495 1496 def update_freebusy_from_attendees(self, attendees): 1497 1498 "For the current user, record free/busy information from 'attendees'." 1499 1500 obj = self.get_stored_object_version() 1501 1502 if not obj or not self.have_new_object(): 1503 return False 1504 1505 # Filter out unrecognised attendees. 1506 1507 attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE"))) 1508 1509 for attendee in attendees: 1510 self.update_freebusy_from_participant(attendee, False) 1511 1512 return True 1513 1514 def remove_freebusy_from_organiser(self, organiser): 1515 1516 "For the current user, remove free/busy information from 'organiser'." 1517 1518 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 1519 1520 def remove_freebusy_from_attendees(self, attendees): 1521 1522 "For the current user, remove free/busy information from 'attendees'." 1523 1524 for attendee in attendees.keys(): 1525 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 1526 1527 # Convenience methods for updating free/busy details at the event level. 1528 1529 def update_event_in_freebusy(self, for_organiser=True): 1530 1531 """ 1532 Update free/busy information when handling an object, doing so for the 1533 organiser of an event if 'for_organiser' is set to a true value. 1534 """ 1535 1536 freebusy = self.store.get_freebusy_for_update(self.user) 1537 1538 # Obtain the attendance attributes for this user, if available. 1539 1540 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 1541 1542 # Remove original recurrence details replaced by additional 1543 # recurrences, as well as obsolete additional recurrences. 1544 1545 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1546 self.store.set_freebusy(self.user, freebusy) 1547 1548 if self.publisher and self.is_sharing() and self.is_publishing(): 1549 self.publisher.set_freebusy(self.user, freebusy) 1550 1551 # Update free/busy provider information if the event may recur 1552 # indefinitely. 1553 1554 if self.possibly_recurring_indefinitely(): 1555 self.store.append_freebusy_provider(self.user, self.obj) 1556 1557 return True 1558 1559 def remove_event_from_freebusy(self): 1560 1561 "Remove free/busy information when handling an object." 1562 1563 freebusy = self.store.get_freebusy_for_update(self.user) 1564 1565 self.remove_from_freebusy(freebusy) 1566 self.remove_freebusy_for_recurrences(freebusy) 1567 self.store.set_freebusy(self.user, freebusy) 1568 1569 if self.publisher and self.is_sharing() and self.is_publishing(): 1570 self.publisher.set_freebusy(self.user, freebusy) 1571 1572 # Update free/busy provider information if the event may recur 1573 # indefinitely. 1574 1575 if self.possibly_recurring_indefinitely(): 1576 self.store.remove_freebusy_provider(self.user, self.obj) 1577 1578 def update_event_in_freebusy_offers(self): 1579 1580 "Update free/busy offers when handling an object." 1581 1582 freebusy = self.store.get_freebusy_offers_for_update(self.user) 1583 1584 # Obtain the attendance attributes for this user, if available. 1585 1586 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 1587 1588 # Remove original recurrence details replaced by additional 1589 # recurrences, as well as obsolete additional recurrences. 1590 1591 self.remove_freebusy_for_recurrences(freebusy, self.get_recurrences()) 1592 self.store.set_freebusy_offers(self.user, freebusy) 1593 1594 return True 1595 1596 def remove_event_from_freebusy_offers(self): 1597 1598 "Remove free/busy offers when handling an object." 1599 1600 freebusy = self.store.get_freebusy_offers_for_update(self.user) 1601 1602 self.remove_from_freebusy(freebusy) 1603 self.remove_freebusy_for_recurrences(freebusy) 1604 self.store.set_freebusy_offers(self.user, freebusy) 1605 1606 return True 1607 1608 # Convenience methods for removing counter-proposals and updating the 1609 # request queue. 1610 1611 def remove_request(self): 1612 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1613 1614 def remove_event(self): 1615 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 1616 1617 def remove_counter(self, attendee): 1618 self.remove_counters([attendee]) 1619 1620 def remove_counters(self, attendees): 1621 for attendee in attendees: 1622 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 1623 1624 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 1625 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1626 1627 # vim: tabstop=4 expandtab shiftwidth=4