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