1 #!/usr/bin/env python 2 3 """ 4 Common calendar client utilities. 5 6 Copyright (C) 2014, 2015 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 import config 24 from imiptools.data import Object, get_address, get_uri, get_window_end, \ 25 is_new_object, make_freebusy, to_part, \ 26 uri_dict, uri_items, uri_parts, uri_values 27 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \ 28 get_duration, get_timestamp 29 from imiptools.period import can_schedule, remove_period, \ 30 remove_additional_periods, remove_affected_period, \ 31 update_freebusy 32 from imiptools.profile import Preferences 33 import imip_store 34 35 class Client: 36 37 "Common handler and manager methods." 38 39 default_window_size = 100 40 organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST" 41 42 def __init__(self, user, messenger=None, store=None, publisher=None, preferences_dir=None): 43 44 """ 45 Initialise a calendar client with the current 'user', plus any 46 'messenger', 'store' and 'publisher' objects, indicating any specific 47 'preferences_dir'. 48 """ 49 50 self.user = user 51 self.messenger = messenger 52 self.store = store or imip_store.FileStore() 53 54 try: 55 self.publisher = publisher or imip_store.FilePublisher() 56 except OSError: 57 self.publisher = None 58 59 self.preferences_dir = preferences_dir 60 self.preferences = None 61 62 # Store-related methods. 63 64 def acquire_lock(self): 65 self.store.acquire_lock(self.user) 66 67 def release_lock(self): 68 self.store.release_lock(self.user) 69 70 # Preferences-related methods. 71 72 def get_preferences(self): 73 if not self.preferences and self.user: 74 self.preferences = Preferences(self.user, self.preferences_dir) 75 return self.preferences 76 77 def get_user_attributes(self): 78 prefs = self.get_preferences() 79 return prefs and prefs.get_all(["CN"]) or {} 80 81 def get_tzid(self): 82 prefs = self.get_preferences() 83 return prefs and prefs.get("TZID") or get_default_timezone() 84 85 def get_window_size(self): 86 prefs = self.get_preferences() 87 try: 88 return prefs and int(prefs.get("window_size")) or self.default_window_size 89 except (TypeError, ValueError): 90 return self.default_window_size 91 92 def get_window_end(self): 93 return get_window_end(self.get_tzid(), self.get_window_size()) 94 95 def is_participating(self): 96 97 "Return participation in the calendar system." 98 99 prefs = self.get_preferences() 100 return prefs and prefs.get("participating", config.PARTICIPATING_DEFAULT) != "no" or False 101 102 def is_sharing(self): 103 104 "Return whether free/busy information is being generally shared." 105 106 prefs = self.get_preferences() 107 return prefs and prefs.get("freebusy_sharing", config.SHARING_DEFAULT) == "share" or False 108 109 def is_bundling(self): 110 111 "Return whether free/busy information is being bundled in messages." 112 113 prefs = self.get_preferences() 114 return prefs and prefs.get("freebusy_bundling", config.BUNDLING_DEFAULT) == "always" or False 115 116 def is_notifying(self): 117 118 "Return whether recipients are notified about free/busy payloads." 119 120 prefs = self.get_preferences() 121 return prefs and prefs.get("freebusy_messages", config.NOTIFYING_DEFAULT) == "notify" or False 122 123 def is_publishing(self): 124 125 "Return whether free/busy information is being published as Web resources." 126 127 prefs = self.get_preferences() 128 return prefs and prefs.get("freebusy_publishing", config.PUBLISHING_DEFAULT) == "publish" or False 129 130 def is_refreshing(self): 131 132 "Return whether a recipient supports requests to refresh event details." 133 134 prefs = self.get_preferences() 135 return prefs and prefs.get("event_refreshing", config.REFRESHING_DEFAULT) == "always" or False 136 137 def allow_add(self): 138 return self.get_add_method_response() in ("add", "refresh") 139 140 def get_add_method_response(self): 141 prefs = self.get_preferences() 142 return prefs and prefs.get("add_method_response", config.ADD_RESPONSE_DEFAULT) or "refresh" 143 144 def get_offer_period(self): 145 146 "Decode a specification in the iCalendar duration format." 147 148 prefs = self.get_preferences() 149 duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT) 150 151 # NOTE: Should probably report an error somehow if None. 152 153 return duration and get_duration(duration) or None 154 155 def get_organiser_replacement(self): 156 prefs = self.get_preferences() 157 return prefs and prefs.get("organiser_replacement", config.ORGANISER_REPLACEMENT_DEFAULT) or "attendee" 158 159 def have_manager(self): 160 return config.MANAGER_INTERFACE 161 162 def get_permitted_values(self): 163 164 """ 165 Decode a specification of one of the following forms... 166 167 <minute values> 168 <hour values>:<minute values> 169 <hour values>:<minute values>:<second values> 170 171 ...with each list of values being comma-separated. 172 """ 173 174 prefs = self.get_preferences() 175 permitted_values = prefs and prefs.get("permitted_times") 176 if permitted_values: 177 try: 178 l = [] 179 for component in permitted_values.split(":")[:3]: 180 if component: 181 l.append(map(int, component.split(","))) 182 else: 183 l.append(None) 184 185 # NOTE: Should probably report an error somehow. 186 187 except ValueError: 188 return None 189 else: 190 l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or []) 191 return l 192 else: 193 return None 194 195 # Common operations on calendar data. 196 197 def update_senders(self, obj=None): 198 199 """ 200 Update sender details in 'obj', or the current object if not indicated, 201 removing SENT-BY attributes for attendees other than the current user if 202 those attributes give the URI of the calendar system. 203 """ 204 205 obj = obj or self.obj 206 calendar_uri = get_uri(self.messenger.sender) 207 for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")): 208 if attendee != self.user: 209 if attendee_attr.get("SENT-BY") == calendar_uri: 210 del attendee_attr["SENT-BY"] 211 else: 212 attendee_attr["SENT-BY"] = calendar_uri 213 214 def update_sender(self, attr): 215 216 "Update the SENT-BY attribute of the 'attr' sender metadata." 217 218 if self.messenger and self.messenger.sender != get_address(self.user): 219 attr["SENT-BY"] = get_uri(self.messenger.sender) 220 221 def get_sending_attendee(self): 222 223 "Return the attendee who sent the current object." 224 225 calendar_uri = get_uri(self.messenger.sender) 226 for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")): 227 if attendee_attr.get("SENT-BY") == calendar_uri: 228 return get_uri(attendee) 229 return None 230 231 def get_periods(self, obj): 232 233 """ 234 Return periods for the given 'obj'. Interpretation of periods can depend 235 on the time zone, which is obtained for the current user. 236 """ 237 238 return obj.get_periods(self.get_tzid(), self.get_window_end()) 239 240 # Store operations. 241 242 def get_stored_object(self, uid, recurrenceid, section=None, username=None): 243 244 """ 245 Return the stored object for the current user, with the given 'uid' and 246 'recurrenceid' from the given 'section' and for the given 'username' (if 247 specified), or from the standard object collection otherwise. 248 """ 249 250 if section == "counters": 251 fragment = self.store.get_counter(self.user, username, uid, recurrenceid) 252 else: 253 fragment = self.store.get_event(self.user, uid, recurrenceid, section) 254 return fragment and Object(fragment) 255 256 # Free/busy operations. 257 258 def get_freebusy_part(self, freebusy=None): 259 260 """ 261 Return a message part containing free/busy information for the user, 262 either specified as 'freebusy' or obtained from the store directly. 263 """ 264 265 if self.is_sharing() and self.is_bundling(): 266 267 # Invent a unique identifier. 268 269 utcnow = get_timestamp() 270 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) 271 272 freebusy = freebusy or self.store.get_freebusy(self.user) 273 274 user_attr = {} 275 self.update_sender(user_attr) 276 return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)]) 277 278 return None 279 280 def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None): 281 282 """ 283 Update the 'freebusy' collection with the given 'periods', indicating a 284 'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a 285 recurrence or the parent event. The 'summary' and 'organiser' must also 286 be provided. 287 288 An optional 'expires' datetime string can be provided to tag a free/busy 289 offer. 290 """ 291 292 update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires) 293 294 class ClientForObject(Client): 295 296 "A client maintaining a specific object." 297 298 def __init__(self, obj, user, messenger=None, store=None, publisher=None, preferences_dir=None): 299 Client.__init__(self, user, messenger, store, publisher, preferences_dir) 300 self.set_object(obj) 301 302 def set_object(self, obj): 303 304 "Set the current object to 'obj', obtaining metadata details." 305 306 self.obj = obj 307 self.uid = obj and self.obj.get_uid() 308 self.recurrenceid = obj and self.obj.get_recurrenceid() 309 self.sequence = obj and self.obj.get_value("SEQUENCE") 310 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 311 312 def set_identity(self, method): 313 314 """ 315 Set the current user for the current object in the context of the given 316 'method'. It is usually set when initialising the handler, using the 317 recipient details, but outgoing messages do not reference the recipient 318 in this way. 319 """ 320 321 pass 322 323 def is_usable(self, method=None): 324 325 "Return whether the current object is usable with the given 'method'." 326 327 return True 328 329 def is_organiser(self): 330 331 """ 332 Return whether the current user is the organiser in the current object. 333 """ 334 335 return get_uri(self.obj.get_value("ORGANIZER")) == self.user 336 337 # Object update methods. 338 339 def update_recurrenceid(self): 340 341 """ 342 Update the RECURRENCE-ID in the current object, initialising it from 343 DTSTART. 344 """ 345 346 self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")] 347 self.recurrenceid = self.obj.get_recurrenceid() 348 349 def update_dtstamp(self, obj=None): 350 351 "Update the DTSTAMP in the current object or any given object 'obj'." 352 353 obj = obj or self.obj 354 self.dtstamp = obj.update_dtstamp() 355 356 def update_sequence(self, increment=False, obj=None): 357 358 "Update the SEQUENCE in the current object or any given object 'obj'." 359 360 obj = obj or self.obj 361 obj.update_sequence(increment) 362 363 def merge_attendance(self, attendees): 364 365 """ 366 Merge attendance from the current object's 'attendees' into the version 367 stored for the current user. 368 """ 369 370 obj = self.get_stored_object_version() 371 372 if not obj or not self.have_new_object(): 373 return False 374 375 # Get attendee details in a usable form. 376 377 attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) 378 379 for attendee, attendee_attr in attendees.items(): 380 381 # Update attendance in the loaded object for any recognised 382 # attendees. 383 384 if attendee_map.has_key(attendee): 385 attendee_map[attendee] = attendee_attr 386 387 # Set the new details and store the object. 388 389 obj["ATTENDEE"] = attendee_map.items() 390 391 # Set a specific recurrence or the complete event if not an additional 392 # occurrence. 393 394 return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node()) 395 396 def update_attendees(self, attendees, removed): 397 398 """ 399 Update the attendees in the current object with the given 'attendees' 400 and 'removed' attendee lists. 401 402 A tuple is returned containing two items: a list of the attendees whose 403 attendance is being proposed (in a counter-proposal), a list of the 404 attendees whose attendance should be cancelled. 405 """ 406 407 to_cancel = [] 408 409 existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or []) 410 existing_attendees_map = dict(existing_attendees) 411 412 # Added attendees are those from the supplied collection not already 413 # present in the object. 414 415 added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees]) 416 417 # NOTE: When countering, no removals will occur, but additions might. 418 419 if added or removed: 420 421 # The organiser can remove existing attendees. 422 423 if removed and self.is_organiser(): 424 remaining = [] 425 426 for attendee, attendee_attr in existing_attendees: 427 if attendee in removed: 428 429 # Only when an event has not been published can 430 # attendees be silently removed. 431 432 if obj.is_shared(): 433 to_cancel.append((attendee, attendee_attr)) 434 else: 435 remaining.append((attendee, attendee_attr)) 436 437 existing_attendees = remaining 438 439 # Attendees (when countering) must only include the current user and 440 # any added attendees. 441 442 elif not self.is_organiser(): 443 existing_attendees = [] 444 445 # Both organisers and attendees (when countering) can add attendees. 446 447 if added: 448 449 # Obtain a mapping from URIs to name details. 450 451 attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)]) 452 453 for attendee in added: 454 attendee = attendee.strip() 455 if attendee: 456 cn = attendee_map.get(attendee) 457 attendee_attr = {"CN" : cn} or {} 458 459 # Only the organiser can reset the participation attributes. 460 461 if self.is_organiser(): 462 attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}) 463 464 existing_attendees.append((attendee, attendee_attr)) 465 466 # Attendees (when countering) must only include the current user and 467 # any added attendees. 468 469 if not self.is_organiser() and self.user not in existing_attendees: 470 user_attr = self.get_user_attributes() 471 user_attr.update(existing_attendees_map.get(self.user) or {}) 472 existing_attendees.append((self.user, user_attr)) 473 474 self.obj["ATTENDEE"] = existing_attendees 475 476 return added, to_cancel 477 478 def update_participation(self, partstat=None): 479 480 """ 481 Update the participation in the current object of the user with the 482 given 'partstat'. 483 """ 484 485 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user) 486 if not attendee_attr: 487 return None 488 if partstat: 489 attendee_attr["PARTSTAT"] = partstat 490 if attendee_attr.has_key("RSVP"): 491 del attendee_attr["RSVP"] 492 self.update_sender(attendee_attr) 493 return attendee_attr 494 495 # Object-related tests. 496 497 def is_recognised_organiser(self, organiser): 498 499 """ 500 Return whether the given 'organiser' is recognised from 501 previously-received details. If no stored details exist, True is 502 returned. 503 """ 504 505 obj = self.get_stored_object_version() 506 if obj: 507 stored_organiser = get_uri(obj.get_value("ORGANIZER")) 508 return stored_organiser == organiser 509 else: 510 return True 511 512 def is_recognised_attendee(self, attendee): 513 514 """ 515 Return whether the given 'attendee' is recognised from 516 previously-received details. If no stored details exist, True is 517 returned. 518 """ 519 520 obj = self.get_stored_object_version() 521 if obj: 522 stored_attendees = uri_dict(obj.get_value_map("ATTENDEE")) 523 return stored_attendees.has_key(attendee) 524 else: 525 return True 526 527 def get_attendance(self, user=None, obj=None): 528 529 """ 530 Return the attendance attributes for 'user', or the current user if 531 'user' is not specified. 532 """ 533 534 attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE")) 535 return attendees.get(user or self.user) 536 537 def is_participating(self, user, as_organiser=False, obj=None): 538 539 """ 540 Return whether, subject to the 'user' indicating an identity and the 541 'as_organiser' status of that identity, the user concerned is actually 542 participating in the current object event. 543 """ 544 545 # Use any attendee property information for an organiser, not the 546 # organiser property attributes. 547 548 attr = self.get_attendance(user, obj=obj) 549 return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") != "DECLINED" 550 551 def get_overriding_transparency(self, user, as_organiser=False): 552 553 """ 554 Return the overriding transparency to be associated with the free/busy 555 records for an event, subject to the 'user' indicating an identity and 556 the 'as_organiser' status of that identity. 557 558 Where an identity is only an organiser and not attending, "ORG" is 559 returned. Otherwise, no overriding transparency is defined and None is 560 returned. 561 """ 562 563 attr = self.get_attendance(user) 564 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 565 566 def can_schedule(self, freebusy, periods): 567 568 """ 569 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 570 """ 571 572 return can_schedule(freebusy, periods, self.uid, self.recurrenceid) 573 574 def have_new_object(self, strict=True): 575 576 """ 577 Return whether the current object is new to the current user. 578 579 If 'strict' is specified and is a false value, the DTSTAMP test will be 580 ignored. This is useful in handling responses from attendees from 581 clients (like Claws Mail) that erase time information from DTSTAMP and 582 make it invalid. 583 """ 584 585 obj = self.get_stored_object_version() 586 587 # If found, compare SEQUENCE and potentially DTSTAMP. 588 589 if obj: 590 sequence = obj.get_value("SEQUENCE") 591 dtstamp = obj.get_value("DTSTAMP") 592 593 # If the request refers to an older version of the object, ignore 594 # it. 595 596 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 597 598 return True 599 600 def possibly_recurring_indefinitely(self): 601 602 "Return whether the object recurs indefinitely." 603 604 # Obtain the stored object to make sure that recurrence information 605 # is not being ignored. This might happen if a client sends a 606 # cancellation without the complete set of properties, for instance. 607 608 return self.obj.possibly_recurring_indefinitely() or \ 609 self.get_stored_object_version() and \ 610 self.get_stored_object_version().possibly_recurring_indefinitely() 611 612 # Constraint application on event periods. 613 614 def check_object(self): 615 616 "Check the object against any scheduling constraints." 617 618 permitted_values = self.get_permitted_values() 619 if not permitted_values: 620 return None 621 622 invalid = [] 623 624 for period in self.obj.get_periods(self.get_tzid()): 625 start = period.get_start() 626 end = period.get_end() 627 start_errors = check_permitted_values(start, permitted_values) 628 end_errors = check_permitted_values(end, permitted_values) 629 if start_errors or end_errors: 630 invalid.append((period.origin, start_errors, end_errors)) 631 632 return invalid 633 634 def correct_object(self): 635 636 "Correct the object according to any scheduling constraints." 637 638 permitted_values = self.get_permitted_values() 639 return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) 640 641 # Object retrieval. 642 643 def get_stored_object_version(self): 644 645 """ 646 Return the stored object to which the current object refers for the 647 current user. 648 """ 649 650 return self.get_stored_object(self.uid, self.recurrenceid) 651 652 def get_definitive_object(self, as_organiser): 653 654 """ 655 Return an object considered definitive for the current transaction, 656 using 'as_organiser' to select the current transaction's object if 657 false, or selecting a stored object if true. 658 """ 659 660 return not as_organiser and self.obj or self.get_stored_object_version() 661 662 def get_parent_object(self): 663 664 """ 665 Return the parent object to which the current object refers for the 666 current user. 667 """ 668 669 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 670 671 # Convenience methods for modifying free/busy collections. 672 673 def get_recurrence_start_point(self, recurrenceid): 674 675 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 676 677 return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) 678 679 def remove_from_freebusy(self, freebusy): 680 681 "Remove this event from the given 'freebusy' collection." 682 683 if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid: 684 remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid)) 685 686 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 687 688 """ 689 Remove from 'freebusy' any original recurrence from parent free/busy 690 details for the current object, if the current object is a specific 691 additional recurrence. Otherwise, remove all additional recurrence 692 information corresponding to 'recurrenceids', or if omitted, all 693 recurrences. 694 """ 695 696 if self.recurrenceid: 697 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 698 remove_affected_period(freebusy, self.uid, recurrenceid) 699 else: 700 # Remove obsolete recurrence periods. 701 702 remove_additional_periods(freebusy, self.uid, recurrenceids) 703 704 # Remove original periods affected by additional recurrences. 705 706 if recurrenceids: 707 for recurrenceid in recurrenceids: 708 recurrenceid = self.get_recurrence_start_point(recurrenceid) 709 remove_affected_period(freebusy, self.uid, recurrenceid) 710 711 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 712 713 """ 714 Update the 'freebusy' collection for this event with the periods and 715 transparency associated with the current object, subject to the 'user' 716 identity and the attendance details provided for them, indicating 717 whether the update is being done 'as_organiser' (for the organiser of 718 an event) or not. 719 720 If 'offer' is set to a true value, any free/busy updates will be tagged 721 with an expiry time. 722 """ 723 724 # Obtain the stored object if the current object is not issued by the 725 # organiser. Attendees do not have the opportunity to redefine the 726 # periods. 727 728 obj = self.get_definitive_object(as_organiser) 729 if not obj: 730 return 731 732 # Obtain the affected periods. 733 734 periods = self.get_periods(obj) 735 736 # Define an overriding transparency, the indicated event transparency, 737 # or the default transparency for the free/busy entry. 738 739 transp = self.get_overriding_transparency(user, as_organiser) or \ 740 obj.get_value("TRANSP") or \ 741 "OPAQUE" 742 743 # Calculate any expiry time. If no offer period is defined, do not 744 # record the offer periods. 745 746 if offer: 747 offer_period = self.get_offer_period() 748 if offer_period: 749 expires = get_timestamp(offer_period) 750 else: 751 return 752 else: 753 expires = None 754 755 # Perform the low-level update. 756 757 Client.update_freebusy(self, freebusy, periods, transp, 758 self.uid, self.recurrenceid, 759 obj.get_value("SUMMARY"), 760 obj.get_value("ORGANIZER"), 761 expires) 762 763 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 764 updating_other=False, offer=False): 765 766 """ 767 Update the 'freebusy' collection for the given 'user', indicating 768 whether the update is 'for_organiser' (being done for the organiser of 769 an event) or not, and whether it is 'updating_other' (meaning another 770 user's details). 771 772 If 'offer' is set to a true value, any free/busy updates will be tagged 773 with an expiry time. 774 """ 775 776 # Record in the free/busy details unless a non-participating attendee. 777 # Remove periods for non-participating attendees. 778 779 if offer or self.is_participating(user, for_organiser and not updating_other): 780 self.update_freebusy(freebusy, user, 781 for_organiser and not updating_other or 782 not for_organiser and updating_other, 783 offer 784 ) 785 else: 786 self.remove_from_freebusy(freebusy) 787 788 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 789 updating_other=False): 790 791 """ 792 Remove details from the 'freebusy' collection for the given 'user', 793 indicating whether the modification is 'for_organiser' (being done for 794 the organiser of an event) or not, and whether it is 'updating_other' 795 (meaning another user's details). 796 """ 797 798 # Remove from the free/busy details if a specified attendee. 799 800 if self.is_participating(user, for_organiser and not updating_other): 801 self.remove_from_freebusy(freebusy) 802 803 # Convenience methods for updating stored free/busy information received 804 # from other users. 805 806 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 807 808 """ 809 For the current user, record the free/busy information for another 810 'user', indicating whether the update is 'for_organiser' or not, thus 811 maintaining a separate record of their free/busy details. 812 """ 813 814 fn = fn or self.update_freebusy_for_participant 815 816 # A user does not store free/busy information for themself as another 817 # party. 818 819 if user == self.user: 820 return 821 822 self.acquire_lock() 823 try: 824 freebusy = self.store.get_freebusy_for_other(self.user, user) 825 fn(freebusy, user, for_organiser, True) 826 827 # Tidy up any obsolete recurrences. 828 829 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 830 self.store.set_freebusy_for_other(self.user, freebusy, user) 831 832 finally: 833 self.release_lock() 834 835 def update_freebusy_from_organiser(self, organiser): 836 837 "For the current user, record free/busy information from 'organiser'." 838 839 self.update_freebusy_from_participant(organiser, True) 840 841 def update_freebusy_from_attendees(self, attendees): 842 843 "For the current user, record free/busy information from 'attendees'." 844 845 obj = self.get_stored_object_version() 846 847 if not obj or not self.have_new_object(): 848 return 849 850 # Filter out unrecognised attendees. 851 852 attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE"))) 853 854 for attendee in attendees: 855 self.update_freebusy_from_participant(attendee, False) 856 857 def remove_freebusy_from_organiser(self, organiser): 858 859 "For the current user, remove free/busy information from 'organiser'." 860 861 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 862 863 def remove_freebusy_from_attendees(self, attendees): 864 865 "For the current user, remove free/busy information from 'attendees'." 866 867 for attendee in attendees.keys(): 868 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 869 870 # Convenience methods for updating free/busy details at the event level. 871 872 def update_event_in_freebusy(self, for_organiser=True): 873 874 """ 875 Update free/busy information when handling an object, doing so for the 876 organiser of an event if 'for_organiser' is set to a true value. 877 """ 878 879 freebusy = self.store.get_freebusy(self.user) 880 881 # Obtain the attendance attributes for this user, if available. 882 883 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 884 885 # Remove original recurrence details replaced by additional 886 # recurrences, as well as obsolete additional recurrences. 887 888 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 889 self.store.set_freebusy(self.user, freebusy) 890 891 if self.publisher and self.is_sharing() and self.is_publishing(): 892 self.publisher.set_freebusy(self.user, freebusy) 893 894 # Update free/busy provider information if the event may recur 895 # indefinitely. 896 897 if self.possibly_recurring_indefinitely(): 898 self.store.append_freebusy_provider(self.user, self.obj) 899 900 return True 901 902 def remove_event_from_freebusy(self): 903 904 "Remove free/busy information when handling an object." 905 906 freebusy = self.store.get_freebusy(self.user) 907 908 self.remove_from_freebusy(freebusy) 909 self.remove_freebusy_for_recurrences(freebusy) 910 self.store.set_freebusy(self.user, freebusy) 911 912 if self.publisher and self.is_sharing() and self.is_publishing(): 913 self.publisher.set_freebusy(self.user, freebusy) 914 915 # Update free/busy provider information if the event may recur 916 # indefinitely. 917 918 if self.possibly_recurring_indefinitely(): 919 self.store.remove_freebusy_provider(self.user, self.obj) 920 921 def update_event_in_freebusy_offers(self): 922 923 "Update free/busy offers when handling an object." 924 925 freebusy = self.store.get_freebusy_offers(self.user) 926 927 # Obtain the attendance attributes for this user, if available. 928 929 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 930 931 # Remove original recurrence details replaced by additional 932 # recurrences, as well as obsolete additional recurrences. 933 934 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 935 self.store.set_freebusy_offers(self.user, freebusy) 936 937 return True 938 939 def remove_event_from_freebusy_offers(self): 940 941 "Remove free/busy offers when handling an object." 942 943 freebusy = self.store.get_freebusy_offers(self.user) 944 945 self.remove_from_freebusy(freebusy) 946 self.remove_freebusy_for_recurrences(freebusy) 947 self.store.set_freebusy_offers(self.user, freebusy) 948 949 return True 950 951 # Convenience methods for removing counter-proposals and updating the 952 # request queue. 953 954 def remove_request(self): 955 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 956 957 def remove_event(self): 958 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 959 960 def remove_counter(self, attendee): 961 self.remove_counters([attendee]) 962 963 def remove_counters(self, attendees): 964 for attendee in attendees: 965 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 966 967 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 968 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 969 970 # vim: tabstop=4 expandtab shiftwidth=4