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_item, 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 # Preparation of messages communicating the state of events. 295 296 def get_message_parts(self, obj, method, attendee=None): 297 298 """ 299 Return a tuple containing a list of methods and a list of message parts, 300 with the parts collectively describing the given object 'obj' and its 301 recurrences, using 'method' as the means of publishing details (with 302 CANCEL being used to retract or remove details). 303 304 If 'attendee' is indicated, the attendee's participation will be taken 305 into account when generating the description. 306 """ 307 308 # Assume that the outcome will be composed of requests and 309 # cancellations. It would not seem completely bizarre to produce 310 # publishing messages if a refresh message was unprovoked. 311 312 responses = [] 313 methods = set() 314 315 # Get the parent event, add SENT-BY details to the organiser. 316 317 if not attendee or self.is_participating(attendee, obj=obj): 318 organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) 319 self.update_sender(organiser_attr) 320 responses.append(obj.to_part(method)) 321 methods.add(method) 322 323 # Get recurrences for parent events. 324 325 if not self.recurrenceid: 326 327 # Collect active and cancelled recurrences. 328 329 for rl, section, rmethod in [ 330 (self.store.get_active_recurrences(self.user, self.uid), None, method), 331 (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"), 332 ]: 333 334 for recurrenceid in rl: 335 336 # Get the recurrence, add SENT-BY details to the organiser. 337 338 obj = self.get_stored_object(self.uid, recurrenceid, section) 339 340 if not attendee or self.is_participating(attendee, obj=obj): 341 organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) 342 self.update_sender(organiser_attr) 343 responses.append(obj.to_part(rmethod)) 344 methods.add(rmethod) 345 346 return methods, responses 347 348 def get_unscheduled_parts(self, periods): 349 350 "Return message parts describing unscheduled 'periods'." 351 352 unscheduled_parts = [] 353 354 if periods: 355 obj = self.obj.copy() 356 obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) 357 358 for p in periods: 359 if not p.origin: 360 continue 361 obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())] 362 obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())] 363 unscheduled_parts.append(obj.to_part("CANCEL")) 364 365 return unscheduled_parts 366 367 class ClientForObject(Client): 368 369 "A client maintaining a specific object." 370 371 def __init__(self, obj, user, messenger=None, store=None, publisher=None, preferences_dir=None): 372 Client.__init__(self, user, messenger, store, publisher, preferences_dir) 373 self.set_object(obj) 374 375 def set_object(self, obj): 376 377 "Set the current object to 'obj', obtaining metadata details." 378 379 self.obj = obj 380 self.uid = obj and self.obj.get_uid() 381 self.recurrenceid = obj and self.obj.get_recurrenceid() 382 self.sequence = obj and self.obj.get_value("SEQUENCE") 383 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 384 385 def set_identity(self, method): 386 387 """ 388 Set the current user for the current object in the context of the given 389 'method'. It is usually set when initialising the handler, using the 390 recipient details, but outgoing messages do not reference the recipient 391 in this way. 392 """ 393 394 pass 395 396 def is_usable(self, method=None): 397 398 "Return whether the current object is usable with the given 'method'." 399 400 return True 401 402 def is_organiser(self): 403 404 """ 405 Return whether the current user is the organiser in the current object. 406 """ 407 408 return get_uri(self.obj.get_value("ORGANIZER")) == self.user 409 410 # Object update methods. 411 412 def update_recurrenceid(self): 413 414 """ 415 Update the RECURRENCE-ID in the current object, initialising it from 416 DTSTART. 417 """ 418 419 self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")] 420 self.recurrenceid = self.obj.get_recurrenceid() 421 422 def update_dtstamp(self, obj=None): 423 424 "Update the DTSTAMP in the current object or any given object 'obj'." 425 426 obj = obj or self.obj 427 self.dtstamp = obj.update_dtstamp() 428 429 def update_sequence(self, increment=False, obj=None): 430 431 "Update the SEQUENCE in the current object or any given object 'obj'." 432 433 obj = obj or self.obj 434 obj.update_sequence(increment) 435 436 def merge_attendance(self, attendees): 437 438 """ 439 Merge attendance from the current object's 'attendees' into the version 440 stored for the current user. 441 """ 442 443 obj = self.get_stored_object_version() 444 445 if not obj or not self.have_new_object(): 446 return False 447 448 # Get attendee details in a usable form. 449 450 attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) 451 452 for attendee, attendee_attr in attendees.items(): 453 454 # Update attendance in the loaded object for any recognised 455 # attendees. 456 457 if attendee_map.has_key(attendee): 458 attendee_map[attendee] = attendee_attr 459 460 # Set the new details and store the object. 461 462 obj["ATTENDEE"] = attendee_map.items() 463 464 # Set a specific recurrence or the complete event if not an additional 465 # occurrence. 466 467 return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node()) 468 469 def update_attendees(self, attendees, removed): 470 471 """ 472 Update the attendees in the current object with the given 'attendees' 473 and 'removed' attendee lists. 474 475 A tuple is returned containing two items: a list of the attendees whose 476 attendance is being proposed (in a counter-proposal), a list of the 477 attendees whose attendance should be cancelled. 478 """ 479 480 to_cancel = [] 481 482 existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or []) 483 existing_attendees_map = dict(existing_attendees) 484 485 # Added attendees are those from the supplied collection not already 486 # present in the object. 487 488 added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees]) 489 490 # NOTE: When countering, no removals will occur, but additions might. 491 492 if added or removed: 493 494 # The organiser can remove existing attendees. 495 496 if removed and self.is_organiser(): 497 remaining = [] 498 499 for attendee, attendee_attr in existing_attendees: 500 if attendee in removed: 501 502 # Only when an event has not been published can 503 # attendees be silently removed. 504 505 if obj.is_shared(): 506 to_cancel.append((attendee, attendee_attr)) 507 else: 508 remaining.append((attendee, attendee_attr)) 509 510 existing_attendees = remaining 511 512 # Attendees (when countering) must only include the current user and 513 # any added attendees. 514 515 elif not self.is_organiser(): 516 existing_attendees = [] 517 518 # Both organisers and attendees (when countering) can add attendees. 519 520 if added: 521 522 # Obtain a mapping from URIs to name details. 523 524 attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)]) 525 526 for attendee in added: 527 attendee = attendee.strip() 528 if attendee: 529 cn = attendee_map.get(attendee) 530 attendee_attr = {"CN" : cn} or {} 531 532 # Only the organiser can reset the participation attributes. 533 534 if self.is_organiser(): 535 attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}) 536 537 existing_attendees.append((attendee, attendee_attr)) 538 539 # Attendees (when countering) must only include the current user and 540 # any added attendees. 541 542 if not self.is_organiser() and self.user not in existing_attendees: 543 user_attr = self.get_user_attributes() 544 user_attr.update(existing_attendees_map.get(self.user) or {}) 545 existing_attendees.append((self.user, user_attr)) 546 547 self.obj["ATTENDEE"] = existing_attendees 548 549 return added, to_cancel 550 551 def update_participation(self, partstat=None): 552 553 """ 554 Update the participation in the current object of the user with the 555 given 'partstat'. 556 """ 557 558 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user) 559 if not attendee_attr: 560 return None 561 if partstat: 562 attendee_attr["PARTSTAT"] = partstat 563 if attendee_attr.has_key("RSVP"): 564 del attendee_attr["RSVP"] 565 self.update_sender(attendee_attr) 566 return attendee_attr 567 568 # Object-related tests. 569 570 def is_recognised_organiser(self, organiser): 571 572 """ 573 Return whether the given 'organiser' is recognised from 574 previously-received details. If no stored details exist, True is 575 returned. 576 """ 577 578 obj = self.get_stored_object_version() 579 if obj: 580 stored_organiser = get_uri(obj.get_value("ORGANIZER")) 581 return stored_organiser == organiser 582 else: 583 return True 584 585 def is_recognised_attendee(self, attendee): 586 587 """ 588 Return whether the given 'attendee' is recognised from 589 previously-received details. If no stored details exist, True is 590 returned. 591 """ 592 593 obj = self.get_stored_object_version() 594 if obj: 595 stored_attendees = uri_dict(obj.get_value_map("ATTENDEE")) 596 return stored_attendees.has_key(attendee) 597 else: 598 return True 599 600 def get_attendance(self, user=None, obj=None): 601 602 """ 603 Return the attendance attributes for 'user', or the current user if 604 'user' is not specified. 605 """ 606 607 attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE")) 608 return attendees.get(user or self.user) 609 610 def is_participating(self, user, as_organiser=False, obj=None): 611 612 """ 613 Return whether, subject to the 'user' indicating an identity and the 614 'as_organiser' status of that identity, the user concerned is actually 615 participating in the current object event. 616 """ 617 618 # Use any attendee property information for an organiser, not the 619 # organiser property attributes. 620 621 attr = self.get_attendance(user, obj=obj) 622 return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") != "DECLINED" 623 624 def get_overriding_transparency(self, user, as_organiser=False): 625 626 """ 627 Return the overriding transparency to be associated with the free/busy 628 records for an event, subject to the 'user' indicating an identity and 629 the 'as_organiser' status of that identity. 630 631 Where an identity is only an organiser and not attending, "ORG" is 632 returned. Otherwise, no overriding transparency is defined and None is 633 returned. 634 """ 635 636 attr = self.get_attendance(user) 637 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 638 639 def can_schedule(self, freebusy, periods): 640 641 """ 642 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 643 """ 644 645 return can_schedule(freebusy, periods, self.uid, self.recurrenceid) 646 647 def have_new_object(self, strict=True): 648 649 """ 650 Return whether the current object is new to the current user. 651 652 If 'strict' is specified and is a false value, the DTSTAMP test will be 653 ignored. This is useful in handling responses from attendees from 654 clients (like Claws Mail) that erase time information from DTSTAMP and 655 make it invalid. 656 """ 657 658 obj = self.get_stored_object_version() 659 660 # If found, compare SEQUENCE and potentially DTSTAMP. 661 662 if obj: 663 sequence = obj.get_value("SEQUENCE") 664 dtstamp = obj.get_value("DTSTAMP") 665 666 # If the request refers to an older version of the object, ignore 667 # it. 668 669 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 670 671 return True 672 673 def possibly_recurring_indefinitely(self): 674 675 "Return whether the object recurs indefinitely." 676 677 # Obtain the stored object to make sure that recurrence information 678 # is not being ignored. This might happen if a client sends a 679 # cancellation without the complete set of properties, for instance. 680 681 return self.obj.possibly_recurring_indefinitely() or \ 682 self.get_stored_object_version() and \ 683 self.get_stored_object_version().possibly_recurring_indefinitely() 684 685 # Constraint application on event periods. 686 687 def check_object(self): 688 689 "Check the object against any scheduling constraints." 690 691 permitted_values = self.get_permitted_values() 692 if not permitted_values: 693 return None 694 695 invalid = [] 696 697 for period in self.obj.get_periods(self.get_tzid()): 698 start = period.get_start() 699 end = period.get_end() 700 start_errors = check_permitted_values(start, permitted_values) 701 end_errors = check_permitted_values(end, permitted_values) 702 if start_errors or end_errors: 703 invalid.append((period.origin, start_errors, end_errors)) 704 705 return invalid 706 707 def correct_object(self): 708 709 "Correct the object according to any scheduling constraints." 710 711 permitted_values = self.get_permitted_values() 712 return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) 713 714 # Object retrieval. 715 716 def get_stored_object_version(self): 717 718 """ 719 Return the stored object to which the current object refers for the 720 current user. 721 """ 722 723 return self.get_stored_object(self.uid, self.recurrenceid) 724 725 def get_definitive_object(self, as_organiser): 726 727 """ 728 Return an object considered definitive for the current transaction, 729 using 'as_organiser' to select the current transaction's object if 730 false, or selecting a stored object if true. 731 """ 732 733 return not as_organiser and self.obj or self.get_stored_object_version() 734 735 def get_parent_object(self): 736 737 """ 738 Return the parent object to which the current object refers for the 739 current user. 740 """ 741 742 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 743 744 def revert_cancellations(self, periods): 745 746 """ 747 Restore cancelled recurrences corresponding to any of the given 748 'periods'. 749 """ 750 751 for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid): 752 obj = self.get_stored_object(self.uid, recurrenceid, "cancellations") 753 if set(self.get_periods(obj)).intersection(periods): 754 self.store.remove_cancellation(self.user, self.uid, recurrenceid) 755 756 # Convenience methods for modifying free/busy collections. 757 758 def get_recurrence_start_point(self, recurrenceid): 759 760 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 761 762 return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) 763 764 def remove_from_freebusy(self, freebusy): 765 766 "Remove this event from the given 'freebusy' collection." 767 768 if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid: 769 remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid)) 770 771 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 772 773 """ 774 Remove from 'freebusy' any original recurrence from parent free/busy 775 details for the current object, if the current object is a specific 776 additional recurrence. Otherwise, remove all additional recurrence 777 information corresponding to 'recurrenceids', or if omitted, all 778 recurrences. 779 """ 780 781 if self.recurrenceid: 782 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 783 remove_affected_period(freebusy, self.uid, recurrenceid) 784 else: 785 # Remove obsolete recurrence periods. 786 787 remove_additional_periods(freebusy, self.uid, recurrenceids) 788 789 # Remove original periods affected by additional recurrences. 790 791 if recurrenceids: 792 for recurrenceid in recurrenceids: 793 recurrenceid = self.get_recurrence_start_point(recurrenceid) 794 remove_affected_period(freebusy, self.uid, recurrenceid) 795 796 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 797 798 """ 799 Update the 'freebusy' collection for this event with the periods and 800 transparency associated with the current object, subject to the 'user' 801 identity and the attendance details provided for them, indicating 802 whether the update is being done 'as_organiser' (for the organiser of 803 an event) or not. 804 805 If 'offer' is set to a true value, any free/busy updates will be tagged 806 with an expiry time. 807 """ 808 809 # Obtain the stored object if the current object is not issued by the 810 # organiser. Attendees do not have the opportunity to redefine the 811 # periods. 812 813 obj = self.get_definitive_object(as_organiser) 814 if not obj: 815 return 816 817 # Obtain the affected periods. 818 819 periods = self.get_periods(obj) 820 821 # Define an overriding transparency, the indicated event transparency, 822 # or the default transparency for the free/busy entry. 823 824 transp = self.get_overriding_transparency(user, as_organiser) or \ 825 obj.get_value("TRANSP") or \ 826 "OPAQUE" 827 828 # Calculate any expiry time. If no offer period is defined, do not 829 # record the offer periods. 830 831 if offer: 832 offer_period = self.get_offer_period() 833 if offer_period: 834 expires = get_timestamp(offer_period) 835 else: 836 return 837 else: 838 expires = None 839 840 # Perform the low-level update. 841 842 Client.update_freebusy(self, freebusy, periods, transp, 843 self.uid, self.recurrenceid, 844 obj.get_value("SUMMARY"), 845 obj.get_value("ORGANIZER"), 846 expires) 847 848 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 849 updating_other=False, offer=False): 850 851 """ 852 Update the 'freebusy' collection for the given 'user', indicating 853 whether the update is 'for_organiser' (being done for the organiser of 854 an event) or not, and whether it is 'updating_other' (meaning another 855 user's details). 856 857 If 'offer' is set to a true value, any free/busy updates will be tagged 858 with an expiry time. 859 """ 860 861 # Record in the free/busy details unless a non-participating attendee. 862 # Remove periods for non-participating attendees. 863 864 if offer or self.is_participating(user, for_organiser and not updating_other): 865 self.update_freebusy(freebusy, user, 866 for_organiser and not updating_other or 867 not for_organiser and updating_other, 868 offer 869 ) 870 else: 871 self.remove_from_freebusy(freebusy) 872 873 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 874 updating_other=False): 875 876 """ 877 Remove details from the 'freebusy' collection for the given 'user', 878 indicating whether the modification is 'for_organiser' (being done for 879 the organiser of an event) or not, and whether it is 'updating_other' 880 (meaning another user's details). 881 """ 882 883 # Remove from the free/busy details if a specified attendee. 884 885 if self.is_participating(user, for_organiser and not updating_other): 886 self.remove_from_freebusy(freebusy) 887 888 # Convenience methods for updating stored free/busy information received 889 # from other users. 890 891 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 892 893 """ 894 For the current user, record the free/busy information for another 895 'user', indicating whether the update is 'for_organiser' or not, thus 896 maintaining a separate record of their free/busy details. 897 """ 898 899 fn = fn or self.update_freebusy_for_participant 900 901 # A user does not store free/busy information for themself as another 902 # party. 903 904 if user == self.user: 905 return 906 907 self.acquire_lock() 908 try: 909 freebusy = self.store.get_freebusy_for_other(self.user, user) 910 fn(freebusy, user, for_organiser, True) 911 912 # Tidy up any obsolete recurrences. 913 914 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 915 self.store.set_freebusy_for_other(self.user, freebusy, user) 916 917 finally: 918 self.release_lock() 919 920 def update_freebusy_from_organiser(self, organiser): 921 922 "For the current user, record free/busy information from 'organiser'." 923 924 self.update_freebusy_from_participant(organiser, True) 925 926 def update_freebusy_from_attendees(self, attendees): 927 928 "For the current user, record free/busy information from 'attendees'." 929 930 obj = self.get_stored_object_version() 931 932 if not obj or not self.have_new_object(): 933 return 934 935 # Filter out unrecognised attendees. 936 937 attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE"))) 938 939 for attendee in attendees: 940 self.update_freebusy_from_participant(attendee, False) 941 942 def remove_freebusy_from_organiser(self, organiser): 943 944 "For the current user, remove free/busy information from 'organiser'." 945 946 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 947 948 def remove_freebusy_from_attendees(self, attendees): 949 950 "For the current user, remove free/busy information from 'attendees'." 951 952 for attendee in attendees.keys(): 953 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 954 955 # Convenience methods for updating free/busy details at the event level. 956 957 def update_event_in_freebusy(self, for_organiser=True): 958 959 """ 960 Update free/busy information when handling an object, doing so for the 961 organiser of an event if 'for_organiser' is set to a true value. 962 """ 963 964 freebusy = self.store.get_freebusy(self.user) 965 966 # Obtain the attendance attributes for this user, if available. 967 968 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 969 970 # Remove original recurrence details replaced by additional 971 # recurrences, as well as obsolete additional recurrences. 972 973 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 974 self.store.set_freebusy(self.user, freebusy) 975 976 if self.publisher and self.is_sharing() and self.is_publishing(): 977 self.publisher.set_freebusy(self.user, freebusy) 978 979 # Update free/busy provider information if the event may recur 980 # indefinitely. 981 982 if self.possibly_recurring_indefinitely(): 983 self.store.append_freebusy_provider(self.user, self.obj) 984 985 return True 986 987 def remove_event_from_freebusy(self): 988 989 "Remove free/busy information when handling an object." 990 991 freebusy = self.store.get_freebusy(self.user) 992 993 self.remove_from_freebusy(freebusy) 994 self.remove_freebusy_for_recurrences(freebusy) 995 self.store.set_freebusy(self.user, freebusy) 996 997 if self.publisher and self.is_sharing() and self.is_publishing(): 998 self.publisher.set_freebusy(self.user, freebusy) 999 1000 # Update free/busy provider information if the event may recur 1001 # indefinitely. 1002 1003 if self.possibly_recurring_indefinitely(): 1004 self.store.remove_freebusy_provider(self.user, self.obj) 1005 1006 def update_event_in_freebusy_offers(self): 1007 1008 "Update free/busy offers when handling an object." 1009 1010 freebusy = self.store.get_freebusy_offers(self.user) 1011 1012 # Obtain the attendance attributes for this user, if available. 1013 1014 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 1015 1016 # Remove original recurrence details replaced by additional 1017 # recurrences, as well as obsolete additional recurrences. 1018 1019 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 1020 self.store.set_freebusy_offers(self.user, freebusy) 1021 1022 return True 1023 1024 def remove_event_from_freebusy_offers(self): 1025 1026 "Remove free/busy offers when handling an object." 1027 1028 freebusy = self.store.get_freebusy_offers(self.user) 1029 1030 self.remove_from_freebusy(freebusy) 1031 self.remove_freebusy_for_recurrences(freebusy) 1032 self.store.set_freebusy_offers(self.user, freebusy) 1033 1034 return True 1035 1036 # Convenience methods for removing counter-proposals and updating the 1037 # request queue. 1038 1039 def remove_request(self): 1040 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1041 1042 def remove_event(self): 1043 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 1044 1045 def remove_counter(self, attendee): 1046 self.remove_counters([attendee]) 1047 1048 def remove_counters(self, attendees): 1049 for attendee in attendees: 1050 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 1051 1052 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 1053 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1054 1055 # vim: tabstop=4 expandtab shiftwidth=4