imip-agent

Annotated imiptools/handlers/person.py

1128:a0043845de02
2016-04-19 Paul Boddie Test changes in attendance for declined recurrences. freebusy-collections
paul@55 1
#!/usr/bin/env python
paul@55 2
paul@55 3
"""
paul@55 4
Handlers for a person for whom scheduling is performed.
paul@146 5
paul@1123 6
Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@55 20
"""
paul@55 21
paul@911 22
from imiptools.data import get_address, uri_dict
paul@418 23
from imiptools.handlers import Handler
paul@683 24
from imiptools.handlers.common import CommonFreebusy, CommonEvent
paul@55 25
paul@725 26
class PersonHandler(CommonEvent, Handler):
paul@55 27
paul@725 28
    "Event handling mechanisms specific to people."
paul@55 29
paul@935 30
    def _process(self, handle, from_organiser=True, **kw):
paul@935 31
paul@935 32
        """
paul@935 33
        Obtain valid organiser and attendee details in order to invoke the given
paul@935 34
        'handle' callable, with 'from_organiser' being indicated to obtain the
paul@935 35
        details. Any additional keyword arguments will be passed to 'handle'.
paul@935 36
        """
paul@935 37
paul@935 38
        oa = self.require_organiser_and_attendees(from_organiser)
paul@935 39
        if not oa:
paul@935 40
            return False
paul@935 41
paul@935 42
        (organiser, organiser_attr), attendees = oa
paul@935 43
        return handle(organiser, attendees, **kw)
paul@935 44
paul@935 45
    def _add(self, organiser, attendees, queue=True):
paul@681 46
paul@734 47
        """
paul@734 48
        Add an event occurrence for the current object or produce a response
paul@734 49
        that requests the event details to be sent again.
paul@734 50
        """
paul@681 51
paul@734 52
        # Request details where configured, doing so for unknown objects anyway.
paul@734 53
paul@737 54
        if self.will_refresh():
paul@737 55
            self.make_refresh()
paul@860 56
            return
paul@681 57
paul@681 58
        # Record the event as a recurrence of the parent object.
paul@681 59
paul@681 60
        self.update_recurrenceid()
paul@681 61
paul@734 62
        # Update the recipient's record of the organiser's schedule.
paul@734 63
paul@734 64
        self.update_freebusy_from_organiser(organiser)
paul@734 65
paul@734 66
        # Set the additional occurrence.
paul@734 67
paul@734 68
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@734 69
paul@860 70
        # Remove any previous cancellations involving this event.
paul@860 71
paul@860 72
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@860 73
paul@681 74
        # Queue any request, if appropriate.
paul@681 75
paul@681 76
        if queue:
paul@681 77
            self.store.queue_request(self.user, self.uid, self.recurrenceid)
paul@681 78
paul@681 79
        return True
paul@681 80
paul@935 81
    def _counter(self, organiser, attendees):
paul@743 82
paul@743 83
        """
paul@743 84
        Record details from a counter-proposal, updating the stored object with
paul@743 85
        attendance information.
paul@743 86
        """
paul@743 87
paul@849 88
        # Update the attendance for the sender.
paul@743 89
paul@849 90
        attendee = self.get_sending_attendee()
paul@849 91
        if not attendee:
paul@849 92
            return False
paul@743 93
paul@865 94
        self.merge_attendance({attendee : attendees[attendee]})
paul@743 95
paul@747 96
        # Queue any counter-proposal for perusal.
paul@747 97
paul@849 98
        self.store.set_counter(self.user, attendee, self.obj.to_node(), self.uid, self.recurrenceid)
paul@747 99
        self.store.queue_request(self.user, self.uid, self.recurrenceid, "COUNTER")
paul@747 100
paul@743 101
        return True
paul@743 102
paul@864 103
    def _cancel(self):
paul@864 104
paul@864 105
        "Record an event cancellation."
paul@864 106
paul@864 107
        # Handle an event being published by the sender to themself.
paul@864 108
paul@864 109
        organiser_item = self.require_organiser()
paul@864 110
        if organiser_item:
paul@864 111
            organiser, organiser_attr = organiser_item
paul@864 112
            if self.user == organiser:
paul@910 113
                self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@864 114
                self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@864 115
                self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@864 116
                self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@864 117
                self.remove_event_from_freebusy()
paul@911 118
                self.remove_freebusy_from_attendees(uri_dict(self.obj.get_value_map("ATTENDEE")))
paul@864 119
                return True
paul@864 120
paul@935 121
        return self._process(self._schedule_for_attendee, queue=False, cancel=True)
paul@864 122
paul@935 123
    def _declinecounter(self, organiser, attendees):
paul@804 124
paul@804 125
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@804 126
paul@804 127
        self.remove_event_from_freebusy_offers()
paul@804 128
        return True
paul@804 129
paul@831 130
    def _publish(self):
paul@831 131
paul@831 132
        "Record details of a published event."
paul@831 133
paul@831 134
        # Handle an event being published by the sender to themself.
paul@831 135
paul@831 136
        organiser_item = self.require_organiser()
paul@831 137
        if organiser_item:
paul@831 138
            organiser, organiser_attr = organiser_item
paul@831 139
            if self.user == organiser:
paul@831 140
                self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@864 141
                self.update_event_in_freebusy()
paul@831 142
                return True
paul@831 143
paul@935 144
        return self._process(self._schedule_for_attendee, queue=False)
paul@831 145
paul@935 146
    def _schedule_for_attendee(self, organiser, attendees, queue=False, cancel=False):
paul@55 147
paul@420 148
        """
paul@420 149
        Record details from the current object given a message originating
paul@420 150
        from an organiser if 'from_organiser' is set to a true value, queuing a
paul@420 151
        request if 'queue' is set to a true value, or cancelling an event if
paul@420 152
        'cancel' is set to a true value.
paul@420 153
        """
paul@420 154
paul@935 155
        # Process for the current user, an attendee.
paul@420 156
paul@935 157
        if not self.have_new_object():
paul@61 158
            return False
paul@55 159
paul@935 160
        # Remove additional recurrences if handling a complete event.
paul@935 161
        # Also remove any previous cancellations involving this event.
paul@55 162
paul@935 163
        if not self.recurrenceid:
paul@935 164
            self.store.remove_recurrences(self.user, self.uid)
paul@935 165
            self.store.remove_cancellations(self.user, self.uid)
paul@935 166
        else:
paul@935 167
            self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@100 168
paul@935 169
        # Queue any request, if appropriate.
paul@381 170
paul@935 171
        if queue:
paul@935 172
            self.store.queue_request(self.user, self.uid, self.recurrenceid)
paul@381 173
paul@935 174
        # Set the complete event or an additional occurrence.
paul@55 175
paul@935 176
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@860 177
paul@935 178
        # Cancel complete events or particular occurrences in recurring
paul@935 179
        # events.
paul@493 180
paul@935 181
        if cancel:
paul@935 182
            self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@100 183
paul@935 184
            # Remove any associated request.
paul@504 185
paul@935 186
            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@935 187
            self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@589 188
paul@935 189
            # No return message will occur to update the free/busy
paul@935 190
            # information, so this is done here using outgoing message
paul@935 191
            # functionality.
paul@697 192
paul@935 193
            self.remove_event_from_freebusy()
paul@268 194
paul@935 195
            # Update the recipient's record of the organiser's schedule.
paul@935 196
paul@935 197
            self.remove_freebusy_from_organiser(organiser)
paul@55 198
paul@100 199
        else:
paul@935 200
            self.update_freebusy_from_organiser(organiser)
paul@61 201
paul@61 202
        return True
paul@61 203
paul@935 204
    def _schedule_for_organiser(self, organiser, attendees):
paul@935 205
paul@935 206
        "As organiser, update attendance from valid attendees."
paul@935 207
paul@1125 208
        # Occurrences that are still part of a parent object are separated,
paul@1125 209
        # attendance information transferred, and the free/busy details updated.
paul@1125 210
paul@1125 211
        if self.is_newly_separated_occurrence():
paul@1125 212
            if self.make_separate_occurrence(for_organiser=True):
paul@1125 213
paul@1125 214
                # Update free/busy details for the event.
paul@1125 215
paul@1125 216
                self.update_event_in_freebusy(for_organiser=True)
paul@1125 217
                return True
paul@1123 218
paul@1123 219
        # Merge the attendance for the received object.
paul@1123 220
paul@1125 221
        elif self.merge_attendance(attendees):
paul@1125 222
            return self.update_freebusy_from_attendees(attendees)
paul@935 223
paul@1125 224
        return False
paul@935 225
paul@935 226
    def _refresh(self, organiser, attendees):
paul@688 227
paul@688 228
        """
paul@688 229
        Respond to a refresh message by providing complete event details to
paul@688 230
        attendees.
paul@688 231
        """
paul@688 232
paul@688 233
        # Filter by stored attendees.
paul@688 234
paul@688 235
        obj = self.get_stored_object_version()
paul@688 236
        stored_attendees = set(obj.get_values("ATTENDEE"))
paul@688 237
        attendees = stored_attendees.intersection(attendees)
paul@688 238
paul@688 239
        if not attendees:
paul@688 240
            return False
paul@688 241
paul@864 242
        # Produce REQUEST and CANCEL results.
paul@688 243
paul@694 244
        for attendee in attendees:
paul@864 245
            methods, parts = self.get_message_parts(obj, "REQUEST", attendee)
paul@864 246
            self.add_results(methods, [get_address(attendee)], parts)
paul@860 247
paul@688 248
        return True
paul@688 249
paul@216 250
class Event(PersonHandler):
paul@67 251
paul@67 252
    "An event handler."
paul@67 253
paul@63 254
    def add(self):
paul@63 255
paul@681 256
        "Queue a suggested additional recurrence for any active event."
paul@63 257
paul@1005 258
        _ = self.get_translator()
paul@1005 259
paul@935 260
        if self.allow_add() and self._process(self._add, queue=True):
paul@1005 261
            return self.wrap(_("An addition to an event has been received."))
paul@63 262
paul@63 263
    def cancel(self):
paul@63 264
paul@142 265
        "Queue a cancellation of any active event."
paul@63 266
paul@1005 267
        _ = self.get_translator()
paul@1005 268
paul@864 269
        if self._cancel():
paul@1005 270
            return self.wrap(_("An event cancellation has been received."), link=False)
paul@63 271
paul@63 272
    def counter(self):
paul@63 273
paul@743 274
        "Record a counter-proposal to a proposed event."
paul@743 275
paul@1005 276
        _ = self.get_translator()
paul@1005 277
paul@935 278
        if self._process(self._counter, from_organiser=False):
paul@1005 279
            return self.wrap(_("A counter proposal to an event invitation has been received."), link=True)
paul@63 280
paul@63 281
    def declinecounter(self):
paul@63 282
paul@804 283
        "Record a rejection of a counter-proposal."
paul@63 284
paul@1005 285
        _ = self.get_translator()
paul@1005 286
paul@935 287
        if self._process(self._declinecounter):
paul@1005 288
            return self.wrap(_("Your counter proposal to an event invitation has been declined."), link=True)
paul@63 289
paul@63 290
    def publish(self):
paul@63 291
paul@139 292
        "Register details of any relevant event."
paul@63 293
paul@1005 294
        _ = self.get_translator()
paul@1005 295
paul@831 296
        if self._publish():
paul@1005 297
            return self.wrap(_("Details of an event have been received."))
paul@63 298
paul@63 299
    def refresh(self):
paul@63 300
paul@688 301
        "Requests to refresh events are handled either here or by the client."
paul@63 302
paul@1005 303
        _ = self.get_translator()
paul@1005 304
paul@688 305
        if self.is_refreshing():
paul@935 306
            return self._process(self._refresh, from_organiser=False)
paul@688 307
        else:
paul@1005 308
            return self.wrap(_("A request for updated event details has been received."))
paul@63 309
paul@61 310
    def reply(self):
paul@61 311
paul@61 312
        "Record replies and notify the recipient."
paul@61 313
paul@1005 314
        _ = self.get_translator()
paul@1005 315
paul@935 316
        if self._process(self._schedule_for_organiser, from_organiser=False):
paul@1005 317
            return self.wrap(_("A reply to an event invitation has been received."))
paul@61 318
paul@61 319
    def request(self):
paul@61 320
paul@61 321
        "Hold requests and notify the recipient."
paul@61 322
paul@1005 323
        _ = self.get_translator()
paul@1005 324
paul@935 325
        if self._process(self._schedule_for_attendee, queue=True):
paul@1005 326
            return self.wrap(_("An event invitation has been received."))
paul@60 327
paul@943 328
class Freebusy(CommonFreebusy, Handler):
paul@55 329
paul@55 330
    "A free/busy handler."
paul@55 331
paul@55 332
    def publish(self):
paul@63 333
paul@110 334
        "Register free/busy information."
paul@110 335
paul@1005 336
        _ = self.get_translator()
paul@1005 337
paul@180 338
        self._record_freebusy(from_organiser=True)
paul@180 339
paul@180 340
        # Produce a message if configured to do so.
paul@63 341
paul@468 342
        if self.is_notifying():
paul@1005 343
            return self.wrap(_("A free/busy update has been received."), link=False)
paul@55 344
paul@55 345
    def reply(self):
paul@55 346
paul@63 347
        "Record replies and notify the recipient."
paul@63 348
paul@1005 349
        _ = self.get_translator()
paul@1005 350
paul@180 351
        self._record_freebusy(from_organiser=False)
paul@180 352
paul@180 353
        # Produce a message if configured to do so.
paul@139 354
paul@468 355
        if self.is_notifying():
paul@1005 356
            return self.wrap(_("A reply to a free/busy request has been received."), link=False)
paul@55 357
paul@55 358
    def request(self):
paul@55 359
paul@55 360
        """
paul@55 361
        Respond to a request by preparing a reply containing free/busy
paul@468 362
        information for the recipient.
paul@55 363
        """
paul@55 364
paul@180 365
        # Produce a reply if configured to do so.
paul@55 366
paul@468 367
        if self.is_sharing():
paul@180 368
            return CommonFreebusy.request(self)
paul@55 369
paul@55 370
# Handler registry.
paul@55 371
paul@55 372
handlers = [
paul@55 373
    ("VFREEBUSY",   Freebusy),
paul@55 374
    ("VEVENT",      Event),
paul@55 375
    ]
paul@55 376
paul@55 377
# vim: tabstop=4 expandtab shiftwidth=4