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 removed = uri_values(removed) 490 491 if added or removed: 492 493 # The organiser can remove existing attendees. 494 495 if removed and self.is_organiser(): 496 remaining = [] 497 498 for attendee, attendee_attr in existing_attendees: 499 if attendee in removed: 500 501 # Only when an event has not been published can 502 # attendees be silently removed. 503 504 if self.obj.is_shared(): 505 to_cancel.append((attendee, attendee_attr)) 506 else: 507 remaining.append((attendee, attendee_attr)) 508 509 existing_attendees = remaining 510 511 # Attendees (when countering) must only include the current user and 512 # any added attendees. 513 514 elif not self.is_organiser(): 515 existing_attendees = [] 516 517 # Both organisers and attendees (when countering) can add attendees. 518 519 if added: 520 521 # Obtain a mapping from URIs to name details. 522 523 attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)]) 524 525 for attendee in added: 526 attendee = attendee.strip() 527 if attendee: 528 cn = attendee_map.get(attendee) 529 attendee_attr = {"CN" : cn} or {} 530 531 # Only the organiser can reset the participation attributes. 532 533 if self.is_organiser(): 534 attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}) 535 536 existing_attendees.append((attendee, attendee_attr)) 537 538 # Attendees (when countering) must only include the current user and 539 # any added attendees. 540 541 if not self.is_organiser() and self.user not in existing_attendees: 542 user_attr = self.get_user_attributes() 543 user_attr.update(existing_attendees_map.get(self.user) or {}) 544 existing_attendees.append((self.user, user_attr)) 545 546 self.obj["ATTENDEE"] = existing_attendees 547 548 return added, to_cancel 549 550 def update_participation(self, partstat=None): 551 552 """ 553 Update the participation in the current object of the user with the 554 given 'partstat'. 555 """ 556 557 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user) 558 if not attendee_attr: 559 return None 560 if partstat: 561 attendee_attr["PARTSTAT"] = partstat 562 if attendee_attr.has_key("RSVP"): 563 del attendee_attr["RSVP"] 564 self.update_sender(attendee_attr) 565 return attendee_attr 566 567 # Object-related tests. 568 569 def is_recognised_organiser(self, organiser): 570 571 """ 572 Return whether the given 'organiser' is recognised from 573 previously-received details. If no stored details exist, True is 574 returned. 575 """ 576 577 obj = self.get_stored_object_version() 578 if obj: 579 stored_organiser = get_uri(obj.get_value("ORGANIZER")) 580 return stored_organiser == organiser 581 else: 582 return True 583 584 def is_recognised_attendee(self, attendee): 585 586 """ 587 Return whether the given 'attendee' is recognised from 588 previously-received details. If no stored details exist, True is 589 returned. 590 """ 591 592 obj = self.get_stored_object_version() 593 if obj: 594 stored_attendees = uri_dict(obj.get_value_map("ATTENDEE")) 595 return stored_attendees.has_key(attendee) 596 else: 597 return True 598 599 def get_attendance(self, user=None, obj=None): 600 601 """ 602 Return the attendance attributes for 'user', or the current user if 603 'user' is not specified. 604 """ 605 606 attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE")) 607 return attendees.get(user or self.user) 608 609 def is_participating(self, user, as_organiser=False, obj=None): 610 611 """ 612 Return whether, subject to the 'user' indicating an identity and the 613 'as_organiser' status of that identity, the user concerned is actually 614 participating in the current object event. 615 """ 616 617 # Use any attendee property information for an organiser, not the 618 # organiser property attributes. 619 620 attr = self.get_attendance(user, obj=obj) 621 return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") != "DECLINED" 622 623 def get_overriding_transparency(self, user, as_organiser=False): 624 625 """ 626 Return the overriding transparency to be associated with the free/busy 627 records for an event, subject to the 'user' indicating an identity and 628 the 'as_organiser' status of that identity. 629 630 Where an identity is only an organiser and not attending, "ORG" is 631 returned. Otherwise, no overriding transparency is defined and None is 632 returned. 633 """ 634 635 attr = self.get_attendance(user) 636 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 637 638 def can_schedule(self, freebusy, periods): 639 640 """ 641 Indicate whether within 'freebusy' the given 'periods' can be scheduled. 642 """ 643 644 return can_schedule(freebusy, periods, self.uid, self.recurrenceid) 645 646 def have_new_object(self, strict=True): 647 648 """ 649 Return whether the current object is new to the current user. 650 651 If 'strict' is specified and is a false value, the DTSTAMP test will be 652 ignored. This is useful in handling responses from attendees from 653 clients (like Claws Mail) that erase time information from DTSTAMP and 654 make it invalid. 655 """ 656 657 obj = self.get_stored_object_version() 658 659 # If found, compare SEQUENCE and potentially DTSTAMP. 660 661 if obj: 662 sequence = obj.get_value("SEQUENCE") 663 dtstamp = obj.get_value("DTSTAMP") 664 665 # If the request refers to an older version of the object, ignore 666 # it. 667 668 return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict) 669 670 return True 671 672 def possibly_recurring_indefinitely(self): 673 674 "Return whether the object recurs indefinitely." 675 676 # Obtain the stored object to make sure that recurrence information 677 # is not being ignored. This might happen if a client sends a 678 # cancellation without the complete set of properties, for instance. 679 680 return self.obj.possibly_recurring_indefinitely() or \ 681 self.get_stored_object_version() and \ 682 self.get_stored_object_version().possibly_recurring_indefinitely() 683 684 # Constraint application on event periods. 685 686 def check_object(self): 687 688 "Check the object against any scheduling constraints." 689 690 permitted_values = self.get_permitted_values() 691 if not permitted_values: 692 return None 693 694 invalid = [] 695 696 for period in self.obj.get_periods(self.get_tzid()): 697 start = period.get_start() 698 end = period.get_end() 699 start_errors = check_permitted_values(start, permitted_values) 700 end_errors = check_permitted_values(end, permitted_values) 701 if start_errors or end_errors: 702 invalid.append((period.origin, start_errors, end_errors)) 703 704 return invalid 705 706 def correct_object(self): 707 708 "Correct the object according to any scheduling constraints." 709 710 permitted_values = self.get_permitted_values() 711 return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values) 712 713 # Object retrieval. 714 715 def get_stored_object_version(self): 716 717 """ 718 Return the stored object to which the current object refers for the 719 current user. 720 """ 721 722 return self.get_stored_object(self.uid, self.recurrenceid) 723 724 def get_definitive_object(self, as_organiser): 725 726 """ 727 Return an object considered definitive for the current transaction, 728 using 'as_organiser' to select the current transaction's object if 729 false, or selecting a stored object if true. 730 """ 731 732 return not as_organiser and self.obj or self.get_stored_object_version() 733 734 def get_parent_object(self): 735 736 """ 737 Return the parent object to which the current object refers for the 738 current user. 739 """ 740 741 return self.recurrenceid and self.get_stored_object(self.uid, None) or None 742 743 def revert_cancellations(self, periods): 744 745 """ 746 Restore cancelled recurrences corresponding to any of the given 747 'periods'. 748 """ 749 750 for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid): 751 obj = self.get_stored_object(self.uid, recurrenceid, "cancellations") 752 if set(self.get_periods(obj)).intersection(periods): 753 self.store.remove_cancellation(self.user, self.uid, recurrenceid) 754 755 # Convenience methods for modifying free/busy collections. 756 757 def get_recurrence_start_point(self, recurrenceid): 758 759 "Get 'recurrenceid' in a form suitable for matching free/busy entries." 760 761 return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid()) 762 763 def remove_from_freebusy(self, freebusy): 764 765 "Remove this event from the given 'freebusy' collection." 766 767 if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid: 768 remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid)) 769 770 def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None): 771 772 """ 773 Remove from 'freebusy' any original recurrence from parent free/busy 774 details for the current object, if the current object is a specific 775 additional recurrence. Otherwise, remove all additional recurrence 776 information corresponding to 'recurrenceids', or if omitted, all 777 recurrences. 778 """ 779 780 if self.recurrenceid: 781 recurrenceid = self.get_recurrence_start_point(self.recurrenceid) 782 remove_affected_period(freebusy, self.uid, recurrenceid) 783 else: 784 # Remove obsolete recurrence periods. 785 786 remove_additional_periods(freebusy, self.uid, recurrenceids) 787 788 # Remove original periods affected by additional recurrences. 789 790 if recurrenceids: 791 for recurrenceid in recurrenceids: 792 recurrenceid = self.get_recurrence_start_point(recurrenceid) 793 remove_affected_period(freebusy, self.uid, recurrenceid) 794 795 def update_freebusy(self, freebusy, user, as_organiser, offer=False): 796 797 """ 798 Update the 'freebusy' collection for this event with the periods and 799 transparency associated with the current object, subject to the 'user' 800 identity and the attendance details provided for them, indicating 801 whether the update is being done 'as_organiser' (for the organiser of 802 an event) or not. 803 804 If 'offer' is set to a true value, any free/busy updates will be tagged 805 with an expiry time. 806 """ 807 808 # Obtain the stored object if the current object is not issued by the 809 # organiser. Attendees do not have the opportunity to redefine the 810 # periods. 811 812 obj = self.get_definitive_object(as_organiser) 813 if not obj: 814 return 815 816 # Obtain the affected periods. 817 818 periods = self.get_periods(obj) 819 820 # Define an overriding transparency, the indicated event transparency, 821 # or the default transparency for the free/busy entry. 822 823 transp = self.get_overriding_transparency(user, as_organiser) or \ 824 obj.get_value("TRANSP") or \ 825 "OPAQUE" 826 827 # Calculate any expiry time. If no offer period is defined, do not 828 # record the offer periods. 829 830 if offer: 831 offer_period = self.get_offer_period() 832 if offer_period: 833 expires = get_timestamp(offer_period) 834 else: 835 return 836 else: 837 expires = None 838 839 # Perform the low-level update. 840 841 Client.update_freebusy(self, freebusy, periods, transp, 842 self.uid, self.recurrenceid, 843 obj.get_value("SUMMARY"), 844 obj.get_value("ORGANIZER"), 845 expires) 846 847 def update_freebusy_for_participant(self, freebusy, user, for_organiser=False, 848 updating_other=False, offer=False): 849 850 """ 851 Update the 'freebusy' collection for the given 'user', indicating 852 whether the update is 'for_organiser' (being done for the organiser of 853 an event) or not, and whether it is 'updating_other' (meaning another 854 user's details). 855 856 If 'offer' is set to a true value, any free/busy updates will be tagged 857 with an expiry time. 858 """ 859 860 # Record in the free/busy details unless a non-participating attendee. 861 # Remove periods for non-participating attendees. 862 863 if offer or self.is_participating(user, for_organiser and not updating_other): 864 self.update_freebusy(freebusy, user, 865 for_organiser and not updating_other or 866 not for_organiser and updating_other, 867 offer 868 ) 869 else: 870 self.remove_from_freebusy(freebusy) 871 872 def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False, 873 updating_other=False): 874 875 """ 876 Remove details from the 'freebusy' collection for the given 'user', 877 indicating whether the modification is 'for_organiser' (being done for 878 the organiser of an event) or not, and whether it is 'updating_other' 879 (meaning another user's details). 880 """ 881 882 # Remove from the free/busy details if a specified attendee. 883 884 if self.is_participating(user, for_organiser and not updating_other): 885 self.remove_from_freebusy(freebusy) 886 887 # Convenience methods for updating stored free/busy information received 888 # from other users. 889 890 def update_freebusy_from_participant(self, user, for_organiser, fn=None): 891 892 """ 893 For the current user, record the free/busy information for another 894 'user', indicating whether the update is 'for_organiser' or not, thus 895 maintaining a separate record of their free/busy details. 896 """ 897 898 fn = fn or self.update_freebusy_for_participant 899 900 # A user does not store free/busy information for themself as another 901 # party. 902 903 if user == self.user: 904 return 905 906 self.acquire_lock() 907 try: 908 freebusy = self.store.get_freebusy_for_other(self.user, user) 909 fn(freebusy, user, for_organiser, True) 910 911 # Tidy up any obsolete recurrences. 912 913 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 914 self.store.set_freebusy_for_other(self.user, freebusy, user) 915 916 finally: 917 self.release_lock() 918 919 def update_freebusy_from_organiser(self, organiser): 920 921 "For the current user, record free/busy information from 'organiser'." 922 923 self.update_freebusy_from_participant(organiser, True) 924 925 def update_freebusy_from_attendees(self, attendees): 926 927 "For the current user, record free/busy information from 'attendees'." 928 929 obj = self.get_stored_object_version() 930 931 if not obj or not self.have_new_object(): 932 return 933 934 # Filter out unrecognised attendees. 935 936 attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE"))) 937 938 for attendee in attendees: 939 self.update_freebusy_from_participant(attendee, False) 940 941 def remove_freebusy_from_organiser(self, organiser): 942 943 "For the current user, remove free/busy information from 'organiser'." 944 945 self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant) 946 947 def remove_freebusy_from_attendees(self, attendees): 948 949 "For the current user, remove free/busy information from 'attendees'." 950 951 for attendee in attendees.keys(): 952 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant) 953 954 # Convenience methods for updating free/busy details at the event level. 955 956 def update_event_in_freebusy(self, for_organiser=True): 957 958 """ 959 Update free/busy information when handling an object, doing so for the 960 organiser of an event if 'for_organiser' is set to a true value. 961 """ 962 963 freebusy = self.store.get_freebusy(self.user) 964 965 # Obtain the attendance attributes for this user, if available. 966 967 self.update_freebusy_for_participant(freebusy, self.user, for_organiser) 968 969 # Remove original recurrence details replaced by additional 970 # recurrences, as well as obsolete additional recurrences. 971 972 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 973 self.store.set_freebusy(self.user, freebusy) 974 975 if self.publisher and self.is_sharing() and self.is_publishing(): 976 self.publisher.set_freebusy(self.user, freebusy) 977 978 # Update free/busy provider information if the event may recur 979 # indefinitely. 980 981 if self.possibly_recurring_indefinitely(): 982 self.store.append_freebusy_provider(self.user, self.obj) 983 984 return True 985 986 def remove_event_from_freebusy(self): 987 988 "Remove free/busy information when handling an object." 989 990 freebusy = self.store.get_freebusy(self.user) 991 992 self.remove_from_freebusy(freebusy) 993 self.remove_freebusy_for_recurrences(freebusy) 994 self.store.set_freebusy(self.user, freebusy) 995 996 if self.publisher and self.is_sharing() and self.is_publishing(): 997 self.publisher.set_freebusy(self.user, freebusy) 998 999 # Update free/busy provider information if the event may recur 1000 # indefinitely. 1001 1002 if self.possibly_recurring_indefinitely(): 1003 self.store.remove_freebusy_provider(self.user, self.obj) 1004 1005 def update_event_in_freebusy_offers(self): 1006 1007 "Update free/busy offers when handling an object." 1008 1009 freebusy = self.store.get_freebusy_offers(self.user) 1010 1011 # Obtain the attendance attributes for this user, if available. 1012 1013 self.update_freebusy_for_participant(freebusy, self.user, offer=True) 1014 1015 # Remove original recurrence details replaced by additional 1016 # recurrences, as well as obsolete additional recurrences. 1017 1018 self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) 1019 self.store.set_freebusy_offers(self.user, freebusy) 1020 1021 return True 1022 1023 def remove_event_from_freebusy_offers(self): 1024 1025 "Remove free/busy offers when handling an object." 1026 1027 freebusy = self.store.get_freebusy_offers(self.user) 1028 1029 self.remove_from_freebusy(freebusy) 1030 self.remove_freebusy_for_recurrences(freebusy) 1031 self.store.set_freebusy_offers(self.user, freebusy) 1032 1033 return True 1034 1035 # Convenience methods for removing counter-proposals and updating the 1036 # request queue. 1037 1038 def remove_request(self): 1039 return self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1040 1041 def remove_event(self): 1042 return self.store.remove_event(self.user, self.uid, self.recurrenceid) 1043 1044 def remove_counter(self, attendee): 1045 self.remove_counters([attendee]) 1046 1047 def remove_counters(self, attendees): 1048 for attendee in attendees: 1049 self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid) 1050 1051 if not self.store.get_counters(self.user, self.uid, self.recurrenceid): 1052 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 1053 1054 # vim: tabstop=4 expandtab shiftwidth=4