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