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