imip-agent

Annotated imiptools/client.py

1069:37921ab84c01
2016-03-06 Paul Boddie Moved imip_store into a new imiptools.stores package as the file module.
paul@441 1
#!/usr/bin/env python
paul@441 2
paul@441 3
"""
paul@441 4
Common calendar client utilities.
paul@441 5
paul@1039 6
Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
paul@441 7
paul@441 8
This program is free software; you can redistribute it and/or modify it under
paul@441 9
the terms of the GNU General Public License as published by the Free Software
paul@441 10
Foundation; either version 3 of the License, or (at your option) any later
paul@441 11
version.
paul@441 12
paul@441 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@441 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@441 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@441 16
details.
paul@441 17
paul@441 18
You should have received a copy of the GNU General Public License along with
paul@441 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@441 20
"""
paul@441 21
paul@740 22
from datetime import datetime, timedelta
paul@749 23
from imiptools import config
paul@606 24
from imiptools.data import Object, get_address, get_uri, get_window_end, \
paul@606 25
                           is_new_object, make_freebusy, to_part, \
paul@864 26
                           uri_dict, uri_item, uri_items, uri_parts, uri_values
paul@669 27
from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
paul@809 28
                            get_duration, get_timestamp
paul@1005 29
from imiptools.i18n import get_translator
paul@959 30
from imiptools.period import can_schedule, remove_event_periods, \
paul@606 31
                             remove_additional_periods, remove_affected_period, \
paul@606 32
                             update_freebusy
paul@443 33
from imiptools.profile import Preferences
paul@1069 34
import imiptools.stores.file
paul@441 35
paul@443 36
class Client:
paul@443 37
paul@443 38
    "Common handler and manager methods."
paul@443 39
paul@467 40
    default_window_size = 100
paul@729 41
    organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST"
paul@467 42
paul@1039 43
    def __init__(self, user, messenger=None, store=None, publisher=None, journal=None,
paul@1039 44
                 preferences_dir=None):
paul@728 45
paul@728 46
        """
paul@728 47
        Initialise a calendar client with the current 'user', plus any
paul@1039 48
        'messenger', 'store', 'publisher' and 'journal' objects, indicating any
paul@1039 49
        specific 'preferences_dir'.
paul@728 50
        """
paul@728 51
paul@443 52
        self.user = user
paul@601 53
        self.messenger = messenger
paul@1069 54
        self.store = store or imiptools.stores.file.FileStore()
paul@1069 55
        self.journal = journal or imiptools.stores.file.FileJournal()
paul@604 56
paul@604 57
        try:
paul@1069 58
            self.publisher = publisher or imiptools.stores.file.FilePublisher()
paul@604 59
        except OSError:
paul@604 60
            self.publisher = None
paul@604 61
paul@639 62
        self.preferences_dir = preferences_dir
paul@443 63
        self.preferences = None
paul@443 64
paul@1005 65
        # Localise the messenger.
paul@1005 66
paul@1005 67
        if self.messenger:
paul@1005 68
            self.messenger.gettext = self.get_translator()
paul@1005 69
paul@1012 70
    def get_store(self):
paul@1012 71
        return self.store
paul@1012 72
paul@1012 73
    def get_publisher(self):
paul@1012 74
        return self.publisher
paul@1012 75
paul@1039 76
    def get_journal(self):
paul@1039 77
        return self.journal
paul@1039 78
paul@730 79
    # Store-related methods.
paul@730 80
paul@730 81
    def acquire_lock(self):
paul@730 82
        self.store.acquire_lock(self.user)
paul@730 83
paul@730 84
    def release_lock(self):
paul@730 85
        self.store.release_lock(self.user)
paul@730 86
paul@730 87
    # Preferences-related methods.
paul@730 88
paul@443 89
    def get_preferences(self):
paul@467 90
        if not self.preferences and self.user:
paul@639 91
            self.preferences = Preferences(self.user, self.preferences_dir)
paul@443 92
        return self.preferences
paul@443 93
paul@1005 94
    def get_locale(self):
paul@1012 95
        prefs = self.get_preferences()
paul@1012 96
        return prefs and prefs.get("LANG", "en", True) or "en"
paul@1005 97
paul@1005 98
    def get_translator(self):
paul@1005 99
        return get_translator([self.get_locale()])
paul@1005 100
paul@791 101
    def get_user_attributes(self):
paul@791 102
        prefs = self.get_preferences()
paul@791 103
        return prefs and prefs.get_all(["CN"]) or {}
paul@791 104
paul@443 105
    def get_tzid(self):
paul@443 106
        prefs = self.get_preferences()
paul@467 107
        return prefs and prefs.get("TZID") or get_default_timezone()
paul@443 108
paul@443 109
    def get_window_size(self):
paul@443 110
        prefs = self.get_preferences()
paul@443 111
        try:
paul@467 112
            return prefs and int(prefs.get("window_size")) or self.default_window_size
paul@443 113
        except (TypeError, ValueError):
paul@467 114
            return self.default_window_size
paul@443 115
paul@443 116
    def get_window_end(self):
paul@443 117
        return get_window_end(self.get_tzid(), self.get_window_size())
paul@443 118
paul@667 119
    def is_participating(self):
paul@748 120
paul@748 121
        "Return participation in the calendar system."
paul@748 122
paul@667 123
        prefs = self.get_preferences()
paul@749 124
        return prefs and prefs.get("participating", config.PARTICIPATING_DEFAULT) != "no" or False
paul@667 125
paul@443 126
    def is_sharing(self):
paul@748 127
paul@748 128
        "Return whether free/busy information is being generally shared."
paul@748 129
paul@467 130
        prefs = self.get_preferences()
paul@749 131
        return prefs and prefs.get("freebusy_sharing", config.SHARING_DEFAULT) == "share" or False
paul@443 132
paul@443 133
    def is_bundling(self):
paul@748 134
paul@748 135
        "Return whether free/busy information is being bundled in messages."
paul@748 136
paul@467 137
        prefs = self.get_preferences()
paul@749 138
        return prefs and prefs.get("freebusy_bundling", config.BUNDLING_DEFAULT) == "always" or False
paul@467 139
paul@467 140
    def is_notifying(self):
paul@748 141
paul@748 142
        "Return whether recipients are notified about free/busy payloads."
paul@748 143
paul@467 144
        prefs = self.get_preferences()
paul@749 145
        return prefs and prefs.get("freebusy_messages", config.NOTIFYING_DEFAULT) == "notify" or False
paul@443 146
paul@748 147
    def is_publishing(self):
paul@748 148
paul@748 149
        "Return whether free/busy information is being published as Web resources."
paul@748 150
paul@748 151
        prefs = self.get_preferences()
paul@749 152
        return prefs and prefs.get("freebusy_publishing", config.PUBLISHING_DEFAULT) == "publish" or False
paul@748 153
paul@688 154
    def is_refreshing(self):
paul@748 155
paul@748 156
        "Return whether a recipient supports requests to refresh event details."
paul@748 157
paul@688 158
        prefs = self.get_preferences()
paul@749 159
        return prefs and prefs.get("event_refreshing", config.REFRESHING_DEFAULT) == "always" or False
paul@688 160
paul@734 161
    def allow_add(self):
paul@734 162
        return self.get_add_method_response() in ("add", "refresh")
paul@734 163
paul@734 164
    def get_add_method_response(self):
paul@728 165
        prefs = self.get_preferences()
paul@749 166
        return prefs and prefs.get("add_method_response", config.ADD_RESPONSE_DEFAULT) or "refresh"
paul@734 167
paul@740 168
    def get_offer_period(self):
paul@740 169
paul@759 170
        "Decode a specification in the iCalendar duration format."
paul@740 171
paul@740 172
        prefs = self.get_preferences()
paul@749 173
        duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT)
paul@740 174
paul@759 175
        # NOTE: Should probably report an error somehow if None.
paul@740 176
paul@759 177
        return duration and get_duration(duration) or None
paul@740 178
paul@734 179
    def get_organiser_replacement(self):
paul@734 180
        prefs = self.get_preferences()
paul@749 181
        return prefs and prefs.get("organiser_replacement", config.ORGANISER_REPLACEMENT_DEFAULT) or "attendee"
paul@728 182
paul@668 183
    def have_manager(self):
paul@749 184
        return config.MANAGER_INTERFACE
paul@668 185
paul@669 186
    def get_permitted_values(self):
paul@655 187
paul@655 188
        """
paul@655 189
        Decode a specification of one of the following forms...
paul@655 190
paul@655 191
        <minute values>
paul@655 192
        <hour values>:<minute values>
paul@655 193
        <hour values>:<minute values>:<second values>
paul@655 194
paul@655 195
        ...with each list of values being comma-separated.
paul@655 196
        """
paul@655 197
paul@655 198
        prefs = self.get_preferences()
paul@669 199
        permitted_values = prefs and prefs.get("permitted_times")
paul@669 200
        if permitted_values:
paul@655 201
            try:
paul@655 202
                l = []
paul@669 203
                for component in permitted_values.split(":")[:3]:
paul@655 204
                    if component:
paul@655 205
                        l.append(map(int, component.split(",")))
paul@655 206
                    else:
paul@655 207
                        l.append(None)
paul@655 208
paul@655 209
            # NOTE: Should probably report an error somehow.
paul@655 210
paul@655 211
            except ValueError:
paul@655 212
                return None
paul@655 213
            else:
paul@655 214
                l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or [])
paul@655 215
                return l
paul@655 216
        else:
paul@655 217
            return None
paul@655 218
paul@581 219
    # Common operations on calendar data.
paul@581 220
paul@584 221
    def update_sender(self, attr):
paul@584 222
paul@584 223
        "Update the SENT-BY attribute of the 'attr' sender metadata."
paul@584 224
paul@584 225
        if self.messenger and self.messenger.sender != get_address(self.user):
paul@584 226
            attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@584 227
paul@941 228
    def get_periods(self, obj, explicit_only=False):
paul@606 229
paul@606 230
        """
paul@606 231
        Return periods for the given 'obj'. Interpretation of periods can depend
paul@941 232
        on the time zone, which is obtained for the current user. If
paul@941 233
        'explicit_only' is set to a true value, only explicit periods will be
paul@941 234
        returned, not rule-based periods.
paul@606 235
        """
paul@606 236
paul@941 237
        return obj.get_periods(self.get_tzid(), not explicit_only and self.get_window_end() or None)
paul@606 238
paul@606 239
    # Store operations.
paul@606 240
paul@766 241
    def get_stored_object(self, uid, recurrenceid, section=None, username=None):
paul@606 242
paul@606 243
        """
paul@606 244
        Return the stored object for the current user, with the given 'uid' and
paul@766 245
        'recurrenceid' from the given 'section' and for the given 'username' (if
paul@766 246
        specified), or from the standard object collection otherwise.
paul@606 247
        """
paul@606 248
paul@755 249
        if section == "counters":
paul@766 250
            fragment = self.store.get_counter(self.user, username, uid, recurrenceid)
paul@755 251
        else:
paul@858 252
            fragment = self.store.get_event(self.user, uid, recurrenceid, section)
paul@606 253
        return fragment and Object(fragment)
paul@606 254
paul@604 255
    # Free/busy operations.
paul@604 256
paul@606 257
    def get_freebusy_part(self, freebusy=None):
paul@604 258
paul@604 259
        """
paul@606 260
        Return a message part containing free/busy information for the user,
paul@606 261
        either specified as 'freebusy' or obtained from the store directly.
paul@604 262
        """
paul@604 263
paul@604 264
        if self.is_sharing() and self.is_bundling():
paul@604 265
paul@604 266
            # Invent a unique identifier.
paul@604 267
paul@604 268
            utcnow = get_timestamp()
paul@604 269
            uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@604 270
paul@606 271
            freebusy = freebusy or self.store.get_freebusy(self.user)
paul@604 272
paul@604 273
            user_attr = {}
paul@604 274
            self.update_sender(user_attr)
paul@604 275
            return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])
paul@604 276
paul@604 277
        return None
paul@604 278
paul@740 279
    def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
paul@606 280
paul@606 281
        """
paul@606 282
        Update the 'freebusy' collection with the given 'periods', indicating a
paul@606 283
        'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a
paul@606 284
        recurrence or the parent event. The 'summary' and 'organiser' must also
paul@606 285
        be provided.
paul@740 286
paul@740 287
        An optional 'expires' datetime string can be provided to tag a free/busy
paul@740 288
        offer.
paul@606 289
        """
paul@606 290
paul@740 291
        update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)
paul@606 292
paul@864 293
    # Preparation of messages communicating the state of events.
paul@864 294
paul@864 295
    def get_message_parts(self, obj, method, attendee=None):
paul@864 296
paul@864 297
        """
paul@864 298
        Return a tuple containing a list of methods and a list of message parts,
paul@864 299
        with the parts collectively describing the given object 'obj' and its
paul@864 300
        recurrences, using 'method' as the means of publishing details (with
paul@864 301
        CANCEL being used to retract or remove details).
paul@864 302
paul@864 303
        If 'attendee' is indicated, the attendee's participation will be taken
paul@864 304
        into account when generating the description.
paul@864 305
        """
paul@864 306
paul@864 307
        # Assume that the outcome will be composed of requests and
paul@864 308
        # cancellations. It would not seem completely bizarre to produce
paul@864 309
        # publishing messages if a refresh message was unprovoked.
paul@864 310
paul@864 311
        responses = []
paul@864 312
        methods = set()
paul@864 313
paul@864 314
        # Get the parent event, add SENT-BY details to the organiser.
paul@864 315
paul@864 316
        if not attendee or self.is_participating(attendee, obj=obj):
paul@864 317
            organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
paul@864 318
            self.update_sender(organiser_attr)
paul@864 319
            responses.append(obj.to_part(method))
paul@864 320
            methods.add(method)
paul@864 321
paul@864 322
        # Get recurrences for parent events.
paul@864 323
paul@864 324
        if not self.recurrenceid:
paul@864 325
paul@864 326
            # Collect active and cancelled recurrences.
paul@864 327
paul@864 328
            for rl, section, rmethod in [
paul@864 329
                (self.store.get_active_recurrences(self.user, self.uid), None, method),
paul@864 330
                (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"),
paul@864 331
                ]:
paul@864 332
paul@864 333
                for recurrenceid in rl:
paul@864 334
paul@864 335
                    # Get the recurrence, add SENT-BY details to the organiser.
paul@864 336
paul@864 337
                    obj = self.get_stored_object(self.uid, recurrenceid, section)
paul@864 338
paul@864 339
                    if not attendee or self.is_participating(attendee, obj=obj):
paul@864 340
                        organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
paul@864 341
                        self.update_sender(organiser_attr)
paul@864 342
                        responses.append(obj.to_part(rmethod))
paul@864 343
                        methods.add(rmethod)
paul@864 344
paul@864 345
        return methods, responses
paul@864 346
paul@601 347
class ClientForObject(Client):
paul@601 348
paul@601 349
    "A client maintaining a specific object."
paul@601 350
paul@1039 351
    def __init__(self, obj, user, messenger=None, store=None, publisher=None,
paul@1039 352
                 journal=None, preferences_dir=None):
paul@1039 353
        Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir)
paul@601 354
        self.set_object(obj)
paul@601 355
paul@601 356
    def set_object(self, obj):
paul@606 357
paul@606 358
        "Set the current object to 'obj', obtaining metadata details."
paul@606 359
paul@601 360
        self.obj = obj
paul@601 361
        self.uid = obj and self.obj.get_uid()
paul@601 362
        self.recurrenceid = obj and self.obj.get_recurrenceid()
paul@601 363
        self.sequence = obj and self.obj.get_value("SEQUENCE")
paul@601 364
        self.dtstamp = obj and self.obj.get_value("DTSTAMP")
paul@601 365
paul@729 366
    def set_identity(self, method):
paul@729 367
paul@729 368
        """
paul@729 369
        Set the current user for the current object in the context of the given
paul@729 370
        'method'. It is usually set when initialising the handler, using the
paul@729 371
        recipient details, but outgoing messages do not reference the recipient
paul@729 372
        in this way.
paul@729 373
        """
paul@729 374
paul@729 375
        pass
paul@729 376
paul@727 377
    def is_usable(self, method=None):
paul@720 378
paul@727 379
        "Return whether the current object is usable with the given 'method'."
paul@720 380
paul@720 381
        return True
paul@720 382
paul@818 383
    def is_organiser(self):
paul@818 384
paul@818 385
        """
paul@818 386
        Return whether the current user is the organiser in the current object.
paul@818 387
        """
paul@818 388
paul@818 389
        return get_uri(self.obj.get_value("ORGANIZER")) == self.user
paul@818 390
paul@1039 391
    # Common operations on calendar data.
paul@1039 392
paul@1039 393
    def update_senders(self, obj=None):
paul@1039 394
paul@1039 395
        """
paul@1039 396
        Update sender details in 'obj', or the current object if not indicated,
paul@1039 397
        removing SENT-BY attributes for attendees other than the current user if
paul@1039 398
        those attributes give the URI of the calendar system.
paul@1039 399
        """
paul@1039 400
paul@1039 401
        obj = obj or self.obj
paul@1039 402
        calendar_uri = self.messenger and get_uri(self.messenger.sender)
paul@1039 403
        for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")):
paul@1039 404
            if attendee != self.user:
paul@1039 405
                if attendee_attr.get("SENT-BY") == calendar_uri:
paul@1039 406
                    del attendee_attr["SENT-BY"]
paul@1039 407
            else:
paul@1039 408
                attendee_attr["SENT-BY"] = calendar_uri
paul@1039 409
paul@1039 410
    def get_sending_attendee(self):
paul@1039 411
paul@1039 412
        "Return the attendee who sent the current object."
paul@1039 413
paul@1039 414
        # Search for the sender of the message or the calendar system address.
paul@1039 415
paul@1039 416
        senders = self.senders or self.messenger and [self.messenger.sender] or []
paul@1039 417
paul@1039 418
        for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):
paul@1039 419
            if get_address(attendee) in senders or \
paul@1039 420
               get_address(attendee_attr.get("SENT-BY")) in senders:
paul@1039 421
                return get_uri(attendee)
paul@1039 422
paul@1039 423
        return None
paul@1039 424
paul@1039 425
    def get_unscheduled_parts(self, periods):
paul@1039 426
paul@1039 427
        "Return message parts describing unscheduled 'periods'."
paul@1039 428
paul@1039 429
        unscheduled_parts = []
paul@1039 430
paul@1039 431
        if periods:
paul@1039 432
            obj = self.obj.copy()
paul@1039 433
            obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])
paul@1039 434
paul@1039 435
            for p in periods:
paul@1039 436
                if not p.origin:
paul@1039 437
                    continue
paul@1039 438
                obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())]
paul@1039 439
                obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())]
paul@1039 440
                unscheduled_parts.append(obj.to_part("CANCEL"))
paul@1039 441
paul@1039 442
        return unscheduled_parts
paul@1039 443
paul@604 444
    # Object update methods.
paul@601 445
paul@676 446
    def update_recurrenceid(self):
paul@676 447
paul@676 448
        """
paul@676 449
        Update the RECURRENCE-ID in the current object, initialising it from
paul@676 450
        DTSTART.
paul@676 451
        """
paul@676 452
paul@680 453
        self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")]
paul@676 454
        self.recurrenceid = self.obj.get_recurrenceid()
paul@676 455
paul@809 456
    def update_dtstamp(self, obj=None):
paul@601 457
paul@809 458
        "Update the DTSTAMP in the current object or any given object 'obj'."
paul@809 459
paul@809 460
        obj = obj or self.obj
paul@809 461
        self.dtstamp = obj.update_dtstamp()
paul@601 462
paul@809 463
    def update_sequence(self, increment=False, obj=None):
paul@601 464
paul@809 465
        "Update the SEQUENCE in the current object or any given object 'obj'."
paul@601 466
paul@809 467
        obj = obj or self.obj
paul@809 468
        obj.update_sequence(increment)
paul@601 469
paul@606 470
    def merge_attendance(self, attendees):
paul@606 471
paul@606 472
        """
paul@606 473
        Merge attendance from the current object's 'attendees' into the version
paul@606 474
        stored for the current user.
paul@606 475
        """
paul@606 476
paul@606 477
        obj = self.get_stored_object_version()
paul@606 478
paul@739 479
        if not obj or not self.have_new_object():
paul@606 480
            return False
paul@606 481
paul@606 482
        # Get attendee details in a usable form.
paul@606 483
paul@606 484
        attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
paul@606 485
paul@606 486
        for attendee, attendee_attr in attendees.items():
paul@606 487
paul@836 488
            # Update attendance in the loaded object for any recognised
paul@836 489
            # attendees.
paul@606 490
paul@836 491
            if attendee_map.has_key(attendee):
paul@836 492
                attendee_map[attendee] = attendee_attr
paul@606 493
paul@606 494
        # Set the new details and store the object.
paul@606 495
paul@606 496
        obj["ATTENDEE"] = attendee_map.items()
paul@606 497
paul@744 498
        # Set a specific recurrence or the complete event if not an additional
paul@744 499
        # occurrence.
paul@606 500
paul@829 501
        return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node())
paul@606 502
paul@818 503
    def update_attendees(self, attendees, removed):
paul@818 504
paul@818 505
        """
paul@818 506
        Update the attendees in the current object with the given 'attendees'
paul@818 507
        and 'removed' attendee lists.
paul@818 508
paul@818 509
        A tuple is returned containing two items: a list of the attendees whose
paul@818 510
        attendance is being proposed (in a counter-proposal), a list of the
paul@818 511
        attendees whose attendance should be cancelled.
paul@818 512
        """
paul@818 513
paul@818 514
        to_cancel = []
paul@818 515
paul@818 516
        existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or [])
paul@818 517
        existing_attendees_map = dict(existing_attendees)
paul@818 518
paul@818 519
        # Added attendees are those from the supplied collection not already
paul@818 520
        # present in the object.
paul@818 521
paul@818 522
        added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees])
paul@906 523
        removed = uri_values(removed)
paul@818 524
paul@818 525
        if added or removed:
paul@818 526
paul@818 527
            # The organiser can remove existing attendees.
paul@818 528
paul@818 529
            if removed and self.is_organiser():
paul@818 530
                remaining = []
paul@818 531
paul@818 532
                for attendee, attendee_attr in existing_attendees:
paul@818 533
                    if attendee in removed:
paul@818 534
paul@818 535
                        # Only when an event has not been published can
paul@818 536
                        # attendees be silently removed.
paul@818 537
paul@906 538
                        if self.obj.is_shared():
paul@818 539
                            to_cancel.append((attendee, attendee_attr))
paul@818 540
                    else:
paul@818 541
                        remaining.append((attendee, attendee_attr))
paul@818 542
paul@818 543
                existing_attendees = remaining
paul@818 544
paul@818 545
            # Attendees (when countering) must only include the current user and
paul@818 546
            # any added attendees.
paul@818 547
paul@818 548
            elif not self.is_organiser():
paul@818 549
                existing_attendees = []
paul@818 550
paul@818 551
            # Both organisers and attendees (when countering) can add attendees.
paul@818 552
paul@818 553
            if added:
paul@818 554
paul@818 555
                # Obtain a mapping from URIs to name details.
paul@818 556
paul@818 557
                attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)])
paul@818 558
paul@818 559
                for attendee in added:
paul@818 560
                    attendee = attendee.strip()
paul@818 561
                    if attendee:
paul@818 562
                        cn = attendee_map.get(attendee)
paul@818 563
                        attendee_attr = {"CN" : cn} or {}
paul@818 564
paul@818 565
                        # Only the organiser can reset the participation attributes.
paul@818 566
paul@818 567
                        if self.is_organiser():
paul@818 568
                            attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})
paul@818 569
paul@818 570
                        existing_attendees.append((attendee, attendee_attr))
paul@818 571
paul@818 572
            # Attendees (when countering) must only include the current user and
paul@818 573
            # any added attendees.
paul@818 574
paul@818 575
            if not self.is_organiser() and self.user not in existing_attendees:
paul@818 576
                user_attr = self.get_user_attributes()
paul@818 577
                user_attr.update(existing_attendees_map.get(self.user) or {})
paul@818 578
                existing_attendees.append((self.user, user_attr))
paul@818 579
paul@818 580
            self.obj["ATTENDEE"] = existing_attendees
paul@818 581
paul@818 582
        return added, to_cancel
paul@818 583
paul@818 584
    def update_participation(self, partstat=None):
paul@818 585
paul@818 586
        """
paul@818 587
        Update the participation in the current object of the user with the
paul@818 588
        given 'partstat'.
paul@818 589
        """
paul@818 590
paul@818 591
        attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user)
paul@818 592
        if not attendee_attr:
paul@818 593
            return None
paul@818 594
        if partstat:
paul@818 595
            attendee_attr["PARTSTAT"] = partstat
paul@818 596
        if attendee_attr.has_key("RSVP"):
paul@818 597
            del attendee_attr["RSVP"]
paul@818 598
        self.update_sender(attendee_attr)
paul@818 599
        return attendee_attr
paul@818 600
paul@909 601
    # Communication methods.
paul@909 602
paul@909 603
    def send_message(self, parts, sender, obj, from_organiser, bcc_sender):
paul@909 604
paul@909 605
        """
paul@909 606
        Send the given 'parts' to the appropriate recipients, also sending a
paul@909 607
        copy to the 'sender'. The 'obj' together with the 'from_organiser' value
paul@909 608
        (which indicates whether the organiser is sending this message) are used
paul@909 609
        to determine the recipients of the message.
paul@909 610
        """
paul@909 611
paul@909 612
        # As organiser, send an invitation to attendees, excluding oneself if
paul@909 613
        # also attending. The updated event will be saved by the outgoing
paul@909 614
        # handler.
paul@909 615
paul@909 616
        organiser = get_uri(obj.get_value("ORGANIZER"))
paul@909 617
        attendees = uri_values(obj.get_values("ATTENDEE"))
paul@909 618
paul@909 619
        if from_organiser:
paul@909 620
            recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]
paul@909 621
        else:
paul@909 622
            recipients = [get_address(organiser)]
paul@909 623
paul@909 624
        # Since the outgoing handler updates this user's free/busy details,
paul@909 625
        # the stored details will probably not have the updated details at
paul@909 626
        # this point, so we update our copy for serialisation as the bundled
paul@909 627
        # free/busy object.
paul@909 628
paul@909 629
        freebusy = self.store.get_freebusy(self.user)
paul@909 630
        self.update_freebusy(freebusy, self.user, from_organiser)
paul@909 631
paul@909 632
        # Bundle free/busy information if appropriate.
paul@909 633
paul@909 634
        part = self.get_freebusy_part(freebusy)
paul@909 635
        if part:
paul@909 636
            parts.append(part)
paul@909 637
paul@909 638
        if recipients or bcc_sender:
paul@909 639
            self._send_message(sender, recipients, parts, bcc_sender)
paul@909 640
paul@909 641
    def _send_message(self, sender, recipients, parts, bcc_sender):
paul@909 642
paul@909 643
        """
paul@909 644
        Send a message, explicitly specifying the 'sender' as an outgoing BCC
paul@909 645
        recipient since the generic calendar user will be the actual sender.
paul@909 646
        """
paul@909 647
paul@1005 648
        if not self.messenger:
paul@1005 649
            return
paul@1005 650
paul@909 651
        if not bcc_sender:
paul@909 652
            message = self.messenger.make_outgoing_message(parts, recipients)
paul@909 653
            self.messenger.sendmail(recipients, message.as_string())
paul@909 654
        else:
paul@909 655
            message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
paul@909 656
            self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
paul@909 657
paul@909 658
    def send_message_to_self(self, parts):
paul@909 659
paul@909 660
        "Send a message composed of the given 'parts' to the given user."
paul@909 661
paul@1005 662
        if not self.messenger:
paul@1005 663
            return
paul@1005 664
paul@909 665
        sender = get_address(self.user)
paul@909 666
        message = self.messenger.make_outgoing_message(parts, [sender])
paul@909 667
        self.messenger.sendmail([sender], message.as_string())
paul@909 668
paul@909 669
    # Action methods.
paul@909 670
paul@909 671
    def process_declined_counter(self, attendee):
paul@909 672
paul@909 673
        "Process a declined counter-proposal."
paul@909 674
paul@909 675
        # Obtain the counter-proposal for the attendee.
paul@909 676
paul@909 677
        obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)
paul@909 678
        if not obj:
paul@909 679
            return False
paul@909 680
paul@909 681
        method = "DECLINECOUNTER"
paul@909 682
        self.update_senders(obj=obj)
paul@909 683
        obj.update_dtstamp()
paul@909 684
        obj.update_sequence(False)
paul@909 685
        self._send_message(get_address(self.user), [get_address(attendee)], [obj.to_part(method)], True)
paul@909 686
        return True
paul@909 687
paul@909 688
    def process_received_request(self, changed=False):
paul@909 689
paul@909 690
        """
paul@909 691
        Process the current request for the current user. Return whether any
paul@909 692
        action was taken. If 'changed' is set to a true value, or if 'attendees'
paul@909 693
        is specified and differs from the stored attendees, a counter-proposal
paul@909 694
        will be sent instead of a reply.
paul@909 695
        """
paul@909 696
paul@909 697
        # Reply only on behalf of this user.
paul@909 698
paul@909 699
        attendee_attr = self.update_participation()
paul@909 700
paul@909 701
        if not attendee_attr:
paul@909 702
            return False
paul@909 703
paul@909 704
        if not changed:
paul@909 705
            self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
paul@909 706
        else:
paul@909 707
            self.update_senders()
paul@909 708
paul@909 709
        self.update_dtstamp()
paul@909 710
        self.update_sequence(False)
paul@909 711
        self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), self.obj, False, True)
paul@909 712
        return True
paul@909 713
paul@909 714
    def process_created_request(self, method, to_cancel=None, to_unschedule=None):
paul@909 715
paul@909 716
        """
paul@909 717
        Process the current request, sending a created request of the given
paul@909 718
        'method' to attendees. Return whether any action was taken.
paul@909 719
paul@909 720
        If 'to_cancel' is specified, a list of participants to be sent cancel
paul@909 721
        messages is provided.
paul@909 722
paul@909 723
        If 'to_unschedule' is specified, a list of periods to be unscheduled is
paul@909 724
        provided.
paul@909 725
        """
paul@909 726
paul@909 727
        # Here, the organiser should be the current user.
paul@909 728
paul@909 729
        organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))
paul@909 730
paul@909 731
        self.update_sender(organiser_attr)
paul@909 732
        self.update_senders()
paul@909 733
        self.update_dtstamp()
paul@909 734
        self.update_sequence(True)
paul@909 735
paul@909 736
        if method == "REQUEST":
paul@909 737
            methods, parts = self.get_message_parts(self.obj, "REQUEST")
paul@909 738
paul@909 739
            # Add message parts with cancelled occurrence information.
paul@909 740
paul@909 741
            unscheduled_parts = self.get_unscheduled_parts(to_unschedule)
paul@909 742
paul@909 743
            # Send the updated event, along with a cancellation for each of the
paul@909 744
            # unscheduled occurrences.
paul@909 745
paul@909 746
            self.send_message(parts + unscheduled_parts, get_address(organiser), self.obj, True, False)
paul@909 747
paul@909 748
            # Since the organiser can update the SEQUENCE but this can leave any
paul@909 749
            # mail/calendar client lagging, issue a PUBLISH message to the
paul@909 750
            # user's address.
paul@909 751
paul@909 752
            methods, parts = self.get_message_parts(self.obj, "PUBLISH")
paul@909 753
            self.send_message_to_self(parts + unscheduled_parts)
paul@909 754
paul@909 755
        # When cancelling, replace the attendees with those for whom the event
paul@909 756
        # is now cancelled.
paul@909 757
paul@909 758
        if method == "CANCEL" or to_cancel:
paul@909 759
            if to_cancel:
paul@909 760
                obj = self.obj.copy()
paul@909 761
                obj["ATTENDEE"] = to_cancel
paul@909 762
            else:
paul@909 763
                obj = self.obj
paul@909 764
paul@909 765
            # Send a cancellation to all uninvited attendees.
paul@909 766
paul@909 767
            parts = [obj.to_part("CANCEL")]
paul@909 768
            self.send_message(parts, get_address(organiser), obj, True, False)
paul@909 769
paul@909 770
            # Issue a CANCEL message to the user's address.
paul@909 771
paul@909 772
            if method == "CANCEL":
paul@909 773
                self.send_message_to_self(parts)
paul@909 774
paul@909 775
        return True
paul@909 776
paul@606 777
    # Object-related tests.
paul@606 778
paul@728 779
    def is_recognised_organiser(self, organiser):
paul@728 780
paul@728 781
        """
paul@728 782
        Return whether the given 'organiser' is recognised from
paul@728 783
        previously-received details. If no stored details exist, True is
paul@728 784
        returned.
paul@728 785
        """
paul@728 786
paul@728 787
        obj = self.get_stored_object_version()
paul@728 788
        if obj:
paul@728 789
            stored_organiser = get_uri(obj.get_value("ORGANIZER"))
paul@728 790
            return stored_organiser == organiser
paul@728 791
        else:
paul@728 792
            return True
paul@728 793
paul@728 794
    def is_recognised_attendee(self, attendee):
paul@728 795
paul@728 796
        """
paul@728 797
        Return whether the given 'attendee' is recognised from
paul@728 798
        previously-received details. If no stored details exist, True is
paul@728 799
        returned.
paul@728 800
        """
paul@728 801
paul@728 802
        obj = self.get_stored_object_version()
paul@728 803
        if obj:
paul@728 804
            stored_attendees = uri_dict(obj.get_value_map("ATTENDEE"))
paul@728 805
            return stored_attendees.has_key(attendee)
paul@728 806
        else:
paul@728 807
            return True
paul@728 808
paul@694 809
    def get_attendance(self, user=None, obj=None):
paul@606 810
paul@606 811
        """
paul@606 812
        Return the attendance attributes for 'user', or the current user if
paul@606 813
        'user' is not specified.
paul@606 814
        """
paul@606 815
paul@694 816
        attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE"))
paul@697 817
        return attendees.get(user or self.user)
paul@606 818
paul@694 819
    def is_participating(self, user, as_organiser=False, obj=None):
paul@609 820
paul@609 821
        """
paul@609 822
        Return whether, subject to the 'user' indicating an identity and the
paul@609 823
        'as_organiser' status of that identity, the user concerned is actually
paul@609 824
        participating in the current object event.
paul@609 825
        """
paul@609 826
paul@697 827
        # Use any attendee property information for an organiser, not the
paul@697 828
        # organiser property attributes.
paul@697 829
paul@917 830
        attr = self.get_attendance(user, obj)
paul@917 831
        return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") not in ("DECLINED", "NEEDS-ACTION")
paul@917 832
paul@917 833
    def has_indicated_attendance(self, user=None, obj=None):
paul@917 834
paul@917 835
        """
paul@917 836
        Return whether the given 'user' (or the current user if not specified)
paul@917 837
        has indicated attendance in the given 'obj' (or the current object if
paul@917 838
        not specified).
paul@917 839
        """
paul@917 840
paul@917 841
        attr = self.get_attendance(user, obj)
paul@917 842
        return attr and attr.get("PARTSTAT") not in (None, "NEEDS-ACTION")
paul@609 843
paul@609 844
    def get_overriding_transparency(self, user, as_organiser=False):
paul@609 845
paul@609 846
        """
paul@609 847
        Return the overriding transparency to be associated with the free/busy
paul@609 848
        records for an event, subject to the 'user' indicating an identity and
paul@609 849
        the 'as_organiser' status of that identity.
paul@609 850
paul@609 851
        Where an identity is only an organiser and not attending, "ORG" is
paul@609 852
        returned. Otherwise, no overriding transparency is defined and None is
paul@609 853
        returned.
paul@609 854
        """
paul@609 855
paul@609 856
        attr = self.get_attendance(user)
paul@609 857
        return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None
paul@609 858
paul@606 859
    def can_schedule(self, freebusy, periods):
paul@606 860
paul@606 861
        """
paul@606 862
        Indicate whether within 'freebusy' the given 'periods' can be scheduled.
paul@606 863
        """
paul@606 864
paul@606 865
        return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
paul@606 866
paul@739 867
    def have_new_object(self, strict=True):
paul@606 868
paul@606 869
        """
paul@739 870
        Return whether the current object is new to the current user.
paul@739 871
paul@739 872
        If 'strict' is specified and is a false value, the DTSTAMP test will be
paul@739 873
        ignored. This is useful in handling responses from attendees from
paul@739 874
        clients (like Claws Mail) that erase time information from DTSTAMP and
paul@739 875
        make it invalid.
paul@606 876
        """
paul@606 877
paul@739 878
        obj = self.get_stored_object_version()
paul@606 879
paul@606 880
        # If found, compare SEQUENCE and potentially DTSTAMP.
paul@606 881
paul@606 882
        if obj:
paul@606 883
            sequence = obj.get_value("SEQUENCE")
paul@606 884
            dtstamp = obj.get_value("DTSTAMP")
paul@606 885
paul@606 886
            # If the request refers to an older version of the object, ignore
paul@606 887
            # it.
paul@606 888
paul@682 889
            return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict)
paul@606 890
paul@606 891
        return True
paul@606 892
paul@672 893
    def possibly_recurring_indefinitely(self):
paul@672 894
paul@672 895
        "Return whether the object recurs indefinitely."
paul@672 896
paul@672 897
        # Obtain the stored object to make sure that recurrence information
paul@672 898
        # is not being ignored. This might happen if a client sends a
paul@672 899
        # cancellation without the complete set of properties, for instance.
paul@672 900
paul@672 901
        return self.obj.possibly_recurring_indefinitely() or \
paul@672 902
               self.get_stored_object_version() and \
paul@672 903
               self.get_stored_object_version().possibly_recurring_indefinitely()
paul@672 904
paul@655 905
    # Constraint application on event periods.
paul@655 906
paul@655 907
    def check_object(self):
paul@655 908
paul@655 909
        "Check the object against any scheduling constraints."
paul@655 910
paul@669 911
        permitted_values = self.get_permitted_values()
paul@669 912
        if not permitted_values:
paul@655 913
            return None
paul@655 914
paul@655 915
        invalid = []
paul@655 916
paul@660 917
        for period in self.obj.get_periods(self.get_tzid()):
paul@941 918
            errors = period.check_permitted(permitted_values)
paul@941 919
            if errors:
paul@941 920
                start_errors, end_errors = errors
paul@656 921
                invalid.append((period.origin, start_errors, end_errors))
paul@655 922
paul@655 923
        return invalid
paul@655 924
paul@660 925
    def correct_object(self):
paul@655 926
paul@660 927
        "Correct the object according to any scheduling constraints."
paul@655 928
paul@669 929
        permitted_values = self.get_permitted_values()
paul@669 930
        return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values)
paul@655 931
paul@941 932
    def correct_period(self, period):
paul@941 933
paul@941 934
        "Correct 'period' according to any scheduling constraints."
paul@941 935
paul@941 936
        permitted_values = self.get_permitted_values()
paul@941 937
        if not permitted_values:
paul@941 938
            return period
paul@941 939
        else:
paul@941 940
            return period.get_corrected(permitted_values)
paul@941 941
paul@606 942
    # Object retrieval.
paul@606 943
paul@606 944
    def get_stored_object_version(self):
paul@606 945
paul@606 946
        """
paul@606 947
        Return the stored object to which the current object refers for the
paul@606 948
        current user.
paul@606 949
        """
paul@606 950
paul@606 951
        return self.get_stored_object(self.uid, self.recurrenceid)
paul@606 952
paul@704 953
    def get_definitive_object(self, as_organiser):
paul@606 954
paul@606 955
        """
paul@606 956
        Return an object considered definitive for the current transaction,
paul@704 957
        using 'as_organiser' to select the current transaction's object if
paul@704 958
        false, or selecting a stored object if true.
paul@606 959
        """
paul@606 960
paul@704 961
        return not as_organiser and self.obj or self.get_stored_object_version()
paul@606 962
paul@606 963
    def get_parent_object(self):
paul@606 964
paul@606 965
        """
paul@606 966
        Return the parent object to which the current object refers for the
paul@606 967
        current user.
paul@606 968
        """
paul@606 969
paul@606 970
        return self.recurrenceid and self.get_stored_object(self.uid, None) or None
paul@606 971
paul@864 972
    def revert_cancellations(self, periods):
paul@864 973
paul@864 974
        """
paul@864 975
        Restore cancelled recurrences corresponding to any of the given
paul@864 976
        'periods'.
paul@864 977
        """
paul@864 978
paul@864 979
        for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid):
paul@864 980
            obj = self.get_stored_object(self.uid, recurrenceid, "cancellations")
paul@864 981
            if set(self.get_periods(obj)).intersection(periods):
paul@864 982
                self.store.remove_cancellation(self.user, self.uid, recurrenceid)
paul@864 983
paul@606 984
    # Convenience methods for modifying free/busy collections.
paul@606 985
paul@606 986
    def get_recurrence_start_point(self, recurrenceid):
paul@606 987
paul@606 988
        "Get 'recurrenceid' in a form suitable for matching free/busy entries."
paul@606 989
paul@627 990
        return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid())
paul@606 991
paul@606 992
    def remove_from_freebusy(self, freebusy):
paul@606 993
paul@606 994
        "Remove this event from the given 'freebusy' collection."
paul@606 995
paul@1043 996
        removed = remove_event_periods(freebusy, self.uid, self.recurrenceid)
paul@1043 997
        if not removed and self.recurrenceid:
paul@1043 998
            return remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))
paul@1043 999
        else:
paul@1043 1000
            return removed
paul@606 1001
paul@606 1002
    def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):
paul@606 1003
paul@606 1004
        """
paul@606 1005
        Remove from 'freebusy' any original recurrence from parent free/busy
paul@606 1006
        details for the current object, if the current object is a specific
paul@606 1007
        additional recurrence. Otherwise, remove all additional recurrence
paul@606 1008
        information corresponding to 'recurrenceids', or if omitted, all
paul@606 1009
        recurrences.
paul@606 1010
        """
paul@606 1011
paul@606 1012
        if self.recurrenceid:
paul@606 1013
            recurrenceid = self.get_recurrence_start_point(self.recurrenceid)
paul@606 1014
            remove_affected_period(freebusy, self.uid, recurrenceid)
paul@606 1015
        else:
paul@606 1016
            # Remove obsolete recurrence periods.
paul@606 1017
paul@606 1018
            remove_additional_periods(freebusy, self.uid, recurrenceids)
paul@606 1019
paul@606 1020
            # Remove original periods affected by additional recurrences.
paul@606 1021
paul@606 1022
            if recurrenceids:
paul@606 1023
                for recurrenceid in recurrenceids:
paul@606 1024
                    recurrenceid = self.get_recurrence_start_point(recurrenceid)
paul@606 1025
                    remove_affected_period(freebusy, self.uid, recurrenceid)
paul@606 1026
paul@740 1027
    def update_freebusy(self, freebusy, user, as_organiser, offer=False):
paul@606 1028
paul@606 1029
        """
paul@606 1030
        Update the 'freebusy' collection for this event with the periods and
paul@606 1031
        transparency associated with the current object, subject to the 'user'
paul@606 1032
        identity and the attendance details provided for them, indicating
paul@704 1033
        whether the update is being done 'as_organiser' (for the organiser of
paul@676 1034
        an event) or not.
paul@740 1035
paul@740 1036
        If 'offer' is set to a true value, any free/busy updates will be tagged
paul@740 1037
        with an expiry time.
paul@606 1038
        """
paul@606 1039
paul@606 1040
        # Obtain the stored object if the current object is not issued by the
paul@606 1041
        # organiser. Attendees do not have the opportunity to redefine the
paul@606 1042
        # periods.
paul@606 1043
paul@704 1044
        obj = self.get_definitive_object(as_organiser)
paul@606 1045
        if not obj:
paul@606 1046
            return
paul@606 1047
paul@606 1048
        # Obtain the affected periods.
paul@606 1049
paul@606 1050
        periods = self.get_periods(obj)
paul@606 1051
paul@606 1052
        # Define an overriding transparency, the indicated event transparency,
paul@606 1053
        # or the default transparency for the free/busy entry.
paul@606 1054
paul@704 1055
        transp = self.get_overriding_transparency(user, as_organiser) or \
paul@606 1056
                 obj.get_value("TRANSP") or \
paul@606 1057
                 "OPAQUE"
paul@606 1058
paul@740 1059
        # Calculate any expiry time. If no offer period is defined, do not
paul@740 1060
        # record the offer periods.
paul@740 1061
paul@740 1062
        if offer:
paul@740 1063
            offer_period = self.get_offer_period()
paul@740 1064
            if offer_period:
paul@759 1065
                expires = get_timestamp(offer_period)
paul@740 1066
            else:
paul@740 1067
                return
paul@740 1068
        else:
paul@740 1069
            expires = None
paul@740 1070
paul@606 1071
        # Perform the low-level update.
paul@606 1072
paul@606 1073
        Client.update_freebusy(self, freebusy, periods, transp,
paul@606 1074
            self.uid, self.recurrenceid,
paul@606 1075
            obj.get_value("SUMMARY"),
paul@915 1076
            get_uri(obj.get_value("ORGANIZER")),
paul@740 1077
            expires)
paul@606 1078
paul@606 1079
    def update_freebusy_for_participant(self, freebusy, user, for_organiser=False,
paul@740 1080
                                        updating_other=False, offer=False):
paul@606 1081
paul@606 1082
        """
paul@695 1083
        Update the 'freebusy' collection for the given 'user', indicating
paul@695 1084
        whether the update is 'for_organiser' (being done for the organiser of
paul@695 1085
        an event) or not, and whether it is 'updating_other' (meaning another
paul@695 1086
        user's details).
paul@740 1087
paul@740 1088
        If 'offer' is set to a true value, any free/busy updates will be tagged
paul@740 1089
        with an expiry time.
paul@606 1090
        """
paul@606 1091
paul@606 1092
        # Record in the free/busy details unless a non-participating attendee.
paul@697 1093
        # Remove periods for non-participating attendees.
paul@606 1094
paul@744 1095
        if offer or self.is_participating(user, for_organiser and not updating_other):
paul@704 1096
            self.update_freebusy(freebusy, user,
paul@704 1097
                for_organiser and not updating_other or
paul@740 1098
                not for_organiser and updating_other,
paul@740 1099
                offer
paul@704 1100
                )
paul@606 1101
        else:
paul@606 1102
            self.remove_from_freebusy(freebusy)
paul@606 1103
paul@697 1104
    def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False,
paul@697 1105
                                        updating_other=False):
paul@697 1106
paul@697 1107
        """
paul@697 1108
        Remove details from the 'freebusy' collection for the given 'user',
paul@697 1109
        indicating whether the modification is 'for_organiser' (being done for
paul@697 1110
        the organiser of an event) or not, and whether it is 'updating_other'
paul@697 1111
        (meaning another user's details).
paul@697 1112
        """
paul@697 1113
paul@697 1114
        # Remove from the free/busy details if a specified attendee.
paul@697 1115
paul@697 1116
        if self.is_participating(user, for_organiser and not updating_other):
paul@697 1117
            self.remove_from_freebusy(freebusy)
paul@697 1118
paul@606 1119
    # Convenience methods for updating stored free/busy information received
paul@606 1120
    # from other users.
paul@606 1121
paul@697 1122
    def update_freebusy_from_participant(self, user, for_organiser, fn=None):
paul@606 1123
paul@606 1124
        """
paul@606 1125
        For the current user, record the free/busy information for another
paul@606 1126
        'user', indicating whether the update is 'for_organiser' or not, thus
paul@606 1127
        maintaining a separate record of their free/busy details.
paul@606 1128
        """
paul@606 1129
paul@697 1130
        fn = fn or self.update_freebusy_for_participant
paul@697 1131
paul@606 1132
        # A user does not store free/busy information for themself as another
paul@606 1133
        # party.
paul@606 1134
paul@606 1135
        if user == self.user:
paul@606 1136
            return
paul@606 1137
paul@730 1138
        self.acquire_lock()
paul@702 1139
        try:
paul@730 1140
            freebusy = self.store.get_freebusy_for_other(self.user, user)
paul@702 1141
            fn(freebusy, user, for_organiser, True)
paul@702 1142
paul@702 1143
            # Tidy up any obsolete recurrences.
paul@606 1144
paul@702 1145
            self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
paul@730 1146
            self.store.set_freebusy_for_other(self.user, freebusy, user)
paul@606 1147
paul@702 1148
        finally:
paul@730 1149
            self.release_lock()
paul@606 1150
paul@606 1151
    def update_freebusy_from_organiser(self, organiser):
paul@606 1152
paul@606 1153
        "For the current user, record free/busy information from 'organiser'."
paul@606 1154
paul@606 1155
        self.update_freebusy_from_participant(organiser, True)
paul@606 1156
paul@606 1157
    def update_freebusy_from_attendees(self, attendees):
paul@606 1158
paul@606 1159
        "For the current user, record free/busy information from 'attendees'."
paul@606 1160
paul@836 1161
        obj = self.get_stored_object_version()
paul@836 1162
paul@836 1163
        if not obj or not self.have_new_object():
paul@836 1164
            return
paul@836 1165
paul@836 1166
        # Filter out unrecognised attendees.
paul@836 1167
paul@836 1168
        attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE")))
paul@836 1169
paul@836 1170
        for attendee in attendees:
paul@606 1171
            self.update_freebusy_from_participant(attendee, False)
paul@606 1172
paul@697 1173
    def remove_freebusy_from_organiser(self, organiser):
paul@697 1174
paul@697 1175
        "For the current user, remove free/busy information from 'organiser'."
paul@697 1176
paul@697 1177
        self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant)
paul@697 1178
paul@697 1179
    def remove_freebusy_from_attendees(self, attendees):
paul@697 1180
paul@697 1181
        "For the current user, remove free/busy information from 'attendees'."
paul@697 1182
paul@697 1183
        for attendee in attendees.keys():
paul@697 1184
            self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant)
paul@697 1185
paul@756 1186
    # Convenience methods for updating free/busy details at the event level.
paul@756 1187
paul@756 1188
    def update_event_in_freebusy(self, for_organiser=True):
paul@756 1189
paul@756 1190
        """
paul@756 1191
        Update free/busy information when handling an object, doing so for the
paul@756 1192
        organiser of an event if 'for_organiser' is set to a true value.
paul@756 1193
        """
paul@756 1194
paul@756 1195
        freebusy = self.store.get_freebusy(self.user)
paul@756 1196
paul@756 1197
        # Obtain the attendance attributes for this user, if available.
paul@756 1198
paul@756 1199
        self.update_freebusy_for_participant(freebusy, self.user, for_organiser)
paul@756 1200
paul@756 1201
        # Remove original recurrence details replaced by additional
paul@756 1202
        # recurrences, as well as obsolete additional recurrences.
paul@756 1203
paul@756 1204
        self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
paul@756 1205
        self.store.set_freebusy(self.user, freebusy)
paul@756 1206
paul@756 1207
        if self.publisher and self.is_sharing() and self.is_publishing():
paul@756 1208
            self.publisher.set_freebusy(self.user, freebusy)
paul@756 1209
paul@756 1210
        # Update free/busy provider information if the event may recur
paul@756 1211
        # indefinitely.
paul@756 1212
paul@756 1213
        if self.possibly_recurring_indefinitely():
paul@756 1214
            self.store.append_freebusy_provider(self.user, self.obj)
paul@756 1215
paul@756 1216
        return True
paul@756 1217
paul@756 1218
    def remove_event_from_freebusy(self):
paul@756 1219
paul@756 1220
        "Remove free/busy information when handling an object."
paul@756 1221
paul@756 1222
        freebusy = self.store.get_freebusy(self.user)
paul@756 1223
paul@756 1224
        self.remove_from_freebusy(freebusy)
paul@756 1225
        self.remove_freebusy_for_recurrences(freebusy)
paul@756 1226
        self.store.set_freebusy(self.user, freebusy)
paul@756 1227
paul@756 1228
        if self.publisher and self.is_sharing() and self.is_publishing():
paul@756 1229
            self.publisher.set_freebusy(self.user, freebusy)
paul@756 1230
paul@756 1231
        # Update free/busy provider information if the event may recur
paul@756 1232
        # indefinitely.
paul@756 1233
paul@756 1234
        if self.possibly_recurring_indefinitely():
paul@756 1235
            self.store.remove_freebusy_provider(self.user, self.obj)
paul@756 1236
paul@756 1237
    def update_event_in_freebusy_offers(self):
paul@756 1238
paul@756 1239
        "Update free/busy offers when handling an object."
paul@756 1240
paul@756 1241
        freebusy = self.store.get_freebusy_offers(self.user)
paul@756 1242
paul@756 1243
        # Obtain the attendance attributes for this user, if available.
paul@756 1244
paul@756 1245
        self.update_freebusy_for_participant(freebusy, self.user, offer=True)
paul@756 1246
paul@756 1247
        # Remove original recurrence details replaced by additional
paul@756 1248
        # recurrences, as well as obsolete additional recurrences.
paul@756 1249
paul@756 1250
        self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
paul@756 1251
        self.store.set_freebusy_offers(self.user, freebusy)
paul@756 1252
paul@756 1253
        return True
paul@756 1254
paul@756 1255
    def remove_event_from_freebusy_offers(self):
paul@756 1256
paul@756 1257
        "Remove free/busy offers when handling an object."
paul@756 1258
paul@756 1259
        freebusy = self.store.get_freebusy_offers(self.user)
paul@756 1260
paul@756 1261
        self.remove_from_freebusy(freebusy)
paul@756 1262
        self.remove_freebusy_for_recurrences(freebusy)
paul@756 1263
        self.store.set_freebusy_offers(self.user, freebusy)
paul@756 1264
paul@756 1265
        return True
paul@756 1266
paul@809 1267
    # Convenience methods for removing counter-proposals and updating the
paul@809 1268
    # request queue.
paul@809 1269
paul@813 1270
    def remove_request(self):
paul@813 1271
        return self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@813 1272
paul@813 1273
    def remove_event(self):
paul@813 1274
        return self.store.remove_event(self.user, self.uid, self.recurrenceid)
paul@813 1275
paul@809 1276
    def remove_counter(self, attendee):
paul@809 1277
        self.remove_counters([attendee])
paul@809 1278
paul@809 1279
    def remove_counters(self, attendees):
paul@809 1280
        for attendee in attendees:
paul@809 1281
            self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid)
paul@809 1282
paul@809 1283
        if not self.store.get_counters(self.user, self.uid, self.recurrenceid):
paul@809 1284
            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@809 1285
paul@441 1286
# vim: tabstop=4 expandtab shiftwidth=4