imip-agent

Annotated imiptools/handlers/person.py

1359:8cb064fcd9f1
2017-10-22 Paul Boddie Reworked various aspects of the recurrence computation implementation, removing explicit sort operations and changing day selection to produce results in order.
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@1336 6
Copyright (C) 2014, 2015, 2016, 2017 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@1336 22
from imiptools.data import get_address
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@1307 96
        # Remove any previous counter-proposals for the event from the attendee.
paul@1307 97
        # If a parent event is involved, remove all proposed recurrences.
paul@1307 98
paul@1307 99
        self.store.remove_counters(self.user, self.uid, self.recurrenceid, attendee)
paul@1307 100
paul@747 101
        # Queue any counter-proposal for perusal.
paul@747 102
paul@849 103
        self.store.set_counter(self.user, attendee, self.obj.to_node(), self.uid, self.recurrenceid)
paul@747 104
        self.store.queue_request(self.user, self.uid, self.recurrenceid, "COUNTER")
paul@747 105
paul@743 106
        return True
paul@743 107
paul@864 108
    def _cancel(self):
paul@864 109
paul@864 110
        "Record an event cancellation."
paul@864 111
paul@864 112
        # Handle an event being published by the sender to themself.
paul@864 113
paul@864 114
        organiser_item = self.require_organiser()
paul@864 115
        if organiser_item:
paul@864 116
            organiser, organiser_attr = organiser_item
paul@864 117
            if self.user == organiser:
paul@910 118
                self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@864 119
                self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@864 120
                self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@864 121
                self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@864 122
                self.remove_event_from_freebusy()
paul@1336 123
                self.remove_freebusy_from_attendees(self.obj.get_uri_map("ATTENDEE"))
paul@864 124
                return True
paul@864 125
paul@935 126
        return self._process(self._schedule_for_attendee, queue=False, cancel=True)
paul@864 127
paul@935 128
    def _declinecounter(self, organiser, attendees):
paul@804 129
paul@804 130
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@804 131
paul@804 132
        self.remove_event_from_freebusy_offers()
paul@804 133
        return True
paul@804 134
paul@831 135
    def _publish(self):
paul@831 136
paul@831 137
        "Record details of a published event."
paul@831 138
paul@831 139
        # Handle an event being published by the sender to themself.
paul@831 140
paul@831 141
        organiser_item = self.require_organiser()
paul@831 142
        if organiser_item:
paul@831 143
            organiser, organiser_attr = organiser_item
paul@831 144
            if self.user == organiser:
paul@831 145
                self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@864 146
                self.update_event_in_freebusy()
paul@831 147
                return True
paul@831 148
paul@935 149
        return self._process(self._schedule_for_attendee, queue=False)
paul@831 150
paul@935 151
    def _schedule_for_attendee(self, organiser, attendees, queue=False, cancel=False):
paul@55 152
paul@420 153
        """
paul@420 154
        Record details from the current object given a message originating
paul@420 155
        from an organiser if 'from_organiser' is set to a true value, queuing a
paul@420 156
        request if 'queue' is set to a true value, or cancelling an event if
paul@420 157
        'cancel' is set to a true value.
paul@420 158
        """
paul@420 159
paul@935 160
        # Process for the current user, an attendee.
paul@420 161
paul@935 162
        if not self.have_new_object():
paul@61 163
            return False
paul@55 164
paul@935 165
        # Remove additional recurrences if handling a complete event.
paul@935 166
        # Also remove any previous cancellations involving this event.
paul@55 167
paul@935 168
        if not self.recurrenceid:
paul@935 169
            self.store.remove_recurrences(self.user, self.uid)
paul@935 170
            self.store.remove_cancellations(self.user, self.uid)
paul@935 171
        else:
paul@935 172
            self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@100 173
paul@935 174
        # Queue any request, if appropriate.
paul@381 175
paul@935 176
        if queue:
paul@935 177
            self.store.queue_request(self.user, self.uid, self.recurrenceid)
paul@381 178
paul@935 179
        # Set the complete event or an additional occurrence.
paul@55 180
paul@935 181
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@860 182
paul@935 183
        # Cancel complete events or particular occurrences in recurring
paul@935 184
        # events.
paul@493 185
paul@935 186
        if cancel:
paul@935 187
            self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@100 188
paul@935 189
            # Remove any associated request.
paul@504 190
paul@935 191
            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@935 192
            self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@589 193
paul@935 194
            # No return message will occur to update the free/busy
paul@935 195
            # information, so this is done here using outgoing message
paul@935 196
            # functionality.
paul@697 197
paul@935 198
            self.remove_event_from_freebusy()
paul@268 199
paul@935 200
            # Update the recipient's record of the organiser's schedule.
paul@935 201
paul@935 202
            self.remove_freebusy_from_organiser(organiser)
paul@55 203
paul@100 204
        else:
paul@935 205
            self.update_freebusy_from_organiser(organiser)
paul@61 206
paul@61 207
        return True
paul@61 208
paul@935 209
    def _schedule_for_organiser(self, organiser, attendees):
paul@935 210
paul@935 211
        "As organiser, update attendance from valid attendees."
paul@935 212
paul@1125 213
        # Occurrences that are still part of a parent object are separated,
paul@1125 214
        # attendance information transferred, and the free/busy details updated.
paul@1125 215
paul@1125 216
        if self.is_newly_separated_occurrence():
paul@1125 217
            if self.make_separate_occurrence(for_organiser=True):
paul@1125 218
paul@1125 219
                # Update free/busy details for the event.
paul@1125 220
paul@1125 221
                self.update_event_in_freebusy(for_organiser=True)
paul@1134 222
paul@1134 223
                # Produce a REQUEST for the created occurrence for other
paul@1134 224
                # attendees of the parent event.
paul@1134 225
paul@1134 226
                obj = self.get_parent_object()
paul@1134 227
                stored_attendees = set(obj.get_values("ATTENDEE"))
paul@1134 228
                attendees = stored_attendees.difference(attendees)
paul@1134 229
paul@1134 230
                for attendee in attendees:
paul@1134 231
                    methods, parts = self.get_message_parts(self.obj, "REQUEST", attendee)
paul@1134 232
                    self.add_results(methods, [get_address(attendee)], parts)
paul@1134 233
paul@1125 234
                return True
paul@1123 235
paul@1123 236
        # Merge the attendance for the received object.
paul@1123 237
paul@1125 238
        elif self.merge_attendance(attendees):
paul@1125 239
            return self.update_freebusy_from_attendees(attendees)
paul@935 240
paul@1125 241
        return False
paul@935 242
paul@935 243
    def _refresh(self, organiser, attendees):
paul@688 244
paul@688 245
        """
paul@688 246
        Respond to a refresh message by providing complete event details to
paul@688 247
        attendees.
paul@688 248
        """
paul@688 249
paul@688 250
        # Filter by stored attendees.
paul@688 251
paul@688 252
        obj = self.get_stored_object_version()
paul@688 253
        stored_attendees = set(obj.get_values("ATTENDEE"))
paul@688 254
        attendees = stored_attendees.intersection(attendees)
paul@688 255
paul@688 256
        if not attendees:
paul@688 257
            return False
paul@688 258
paul@864 259
        # Produce REQUEST and CANCEL results.
paul@688 260
paul@694 261
        for attendee in attendees:
paul@864 262
            methods, parts = self.get_message_parts(obj, "REQUEST", attendee)
paul@864 263
            self.add_results(methods, [get_address(attendee)], parts)
paul@860 264
paul@688 265
        return True
paul@688 266
paul@216 267
class Event(PersonHandler):
paul@67 268
paul@67 269
    "An event handler."
paul@67 270
paul@63 271
    def add(self):
paul@63 272
paul@681 273
        "Queue a suggested additional recurrence for any active event."
paul@63 274
paul@1005 275
        _ = self.get_translator()
paul@1005 276
paul@935 277
        if self.allow_add() and self._process(self._add, queue=True):
paul@1151 278
            self.wrap(_("An addition to an event has been received."))
paul@63 279
paul@63 280
    def cancel(self):
paul@63 281
paul@142 282
        "Queue a cancellation of any active event."
paul@63 283
paul@1005 284
        _ = self.get_translator()
paul@1005 285
paul@864 286
        if self._cancel():
paul@1151 287
            self.wrap(_("An event cancellation has been received."), link=False)
paul@63 288
paul@63 289
    def counter(self):
paul@63 290
paul@743 291
        "Record a counter-proposal to a proposed event."
paul@743 292
paul@1005 293
        _ = self.get_translator()
paul@1005 294
paul@935 295
        if self._process(self._counter, from_organiser=False):
paul@1151 296
            self.wrap(_("A counter proposal to an event invitation has been received."), link=True)
paul@63 297
paul@63 298
    def declinecounter(self):
paul@63 299
paul@804 300
        "Record a rejection of a counter-proposal."
paul@63 301
paul@1005 302
        _ = self.get_translator()
paul@1005 303
paul@935 304
        if self._process(self._declinecounter):
paul@1151 305
            self.wrap(_("Your counter proposal to an event invitation has been declined."), link=True)
paul@63 306
paul@63 307
    def publish(self):
paul@63 308
paul@139 309
        "Register details of any relevant event."
paul@63 310
paul@1005 311
        _ = self.get_translator()
paul@1005 312
paul@831 313
        if self._publish():
paul@1151 314
            self.wrap(_("Details of an event have been received."))
paul@63 315
paul@63 316
    def refresh(self):
paul@63 317
paul@688 318
        "Requests to refresh events are handled either here or by the client."
paul@63 319
paul@1005 320
        _ = self.get_translator()
paul@1005 321
paul@688 322
        if self.is_refreshing():
paul@1151 323
            self._process(self._refresh, from_organiser=False)
paul@688 324
        else:
paul@1151 325
            self.wrap(_("A request for updated event details has been received."))
paul@63 326
paul@61 327
    def reply(self):
paul@61 328
paul@61 329
        "Record replies and notify the recipient."
paul@61 330
paul@1005 331
        _ = self.get_translator()
paul@1005 332
paul@935 333
        if self._process(self._schedule_for_organiser, from_organiser=False):
paul@1151 334
            self.wrap(_("A reply to an event invitation has been received."))
paul@61 335
paul@61 336
    def request(self):
paul@61 337
paul@61 338
        "Hold requests and notify the recipient."
paul@61 339
paul@1005 340
        _ = self.get_translator()
paul@1005 341
paul@935 342
        if self._process(self._schedule_for_attendee, queue=True):
paul@1151 343
            self.wrap(_("An event invitation has been received."))
paul@60 344
paul@943 345
class Freebusy(CommonFreebusy, Handler):
paul@55 346
paul@55 347
    "A free/busy handler."
paul@55 348
paul@55 349
    def publish(self):
paul@63 350
paul@110 351
        "Register free/busy information."
paul@110 352
paul@1005 353
        _ = self.get_translator()
paul@1005 354
paul@180 355
        self._record_freebusy(from_organiser=True)
paul@180 356
paul@180 357
        # Produce a message if configured to do so.
paul@63 358
paul@468 359
        if self.is_notifying():
paul@1151 360
            self.wrap(_("A free/busy update has been received."), link=False)
paul@55 361
paul@55 362
    def reply(self):
paul@55 363
paul@63 364
        "Record replies and notify the recipient."
paul@63 365
paul@1005 366
        _ = self.get_translator()
paul@1005 367
paul@180 368
        self._record_freebusy(from_organiser=False)
paul@180 369
paul@180 370
        # Produce a message if configured to do so.
paul@139 371
paul@468 372
        if self.is_notifying():
paul@1151 373
            self.wrap(_("A reply to a free/busy request has been received."), link=False)
paul@55 374
paul@55 375
    def request(self):
paul@55 376
paul@55 377
        """
paul@55 378
        Respond to a request by preparing a reply containing free/busy
paul@468 379
        information for the recipient.
paul@55 380
        """
paul@55 381
paul@180 382
        # Produce a reply if configured to do so.
paul@55 383
paul@468 384
        if self.is_sharing():
paul@180 385
            return CommonFreebusy.request(self)
paul@55 386
paul@55 387
# Handler registry.
paul@55 388
paul@55 389
handlers = [
paul@55 390
    ("VFREEBUSY",   Freebusy),
paul@55 391
    ("VEVENT",      Event),
paul@55 392
    ]
paul@55 393
paul@55 394
# vim: tabstop=4 expandtab shiftwidth=4