imip-agent

Annotated imiptools/handlers/person.py

1045:5a9722130d25
2016-02-08 Paul Boddie Used event-related attributes stored in the handler directly.
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@146 6
Copyright (C) 2014, 2015 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@935 208
        if self.merge_attendance(attendees):
paul@935 209
            self.update_freebusy_from_attendees(attendees)
paul@935 210
paul@935 211
        return True
paul@935 212
paul@935 213
    def _refresh(self, organiser, attendees):
paul@688 214
paul@688 215
        """
paul@688 216
        Respond to a refresh message by providing complete event details to
paul@688 217
        attendees.
paul@688 218
        """
paul@688 219
paul@688 220
        # Filter by stored attendees.
paul@688 221
paul@688 222
        obj = self.get_stored_object_version()
paul@688 223
        stored_attendees = set(obj.get_values("ATTENDEE"))
paul@688 224
        attendees = stored_attendees.intersection(attendees)
paul@688 225
paul@688 226
        if not attendees:
paul@688 227
            return False
paul@688 228
paul@864 229
        # Produce REQUEST and CANCEL results.
paul@688 230
paul@694 231
        for attendee in attendees:
paul@864 232
            methods, parts = self.get_message_parts(obj, "REQUEST", attendee)
paul@864 233
            self.add_results(methods, [get_address(attendee)], parts)
paul@860 234
paul@688 235
        return True
paul@688 236
paul@216 237
class Event(PersonHandler):
paul@67 238
paul@67 239
    "An event handler."
paul@67 240
paul@63 241
    def add(self):
paul@63 242
paul@681 243
        "Queue a suggested additional recurrence for any active event."
paul@63 244
paul@1005 245
        _ = self.get_translator()
paul@1005 246
paul@935 247
        if self.allow_add() and self._process(self._add, queue=True):
paul@1005 248
            return self.wrap(_("An addition to an event has been received."))
paul@63 249
paul@63 250
    def cancel(self):
paul@63 251
paul@142 252
        "Queue a cancellation of any active event."
paul@63 253
paul@1005 254
        _ = self.get_translator()
paul@1005 255
paul@864 256
        if self._cancel():
paul@1005 257
            return self.wrap(_("An event cancellation has been received."), link=False)
paul@63 258
paul@63 259
    def counter(self):
paul@63 260
paul@743 261
        "Record a counter-proposal to a proposed event."
paul@743 262
paul@1005 263
        _ = self.get_translator()
paul@1005 264
paul@935 265
        if self._process(self._counter, from_organiser=False):
paul@1005 266
            return self.wrap(_("A counter proposal to an event invitation has been received."), link=True)
paul@63 267
paul@63 268
    def declinecounter(self):
paul@63 269
paul@804 270
        "Record a rejection of a counter-proposal."
paul@63 271
paul@1005 272
        _ = self.get_translator()
paul@1005 273
paul@935 274
        if self._process(self._declinecounter):
paul@1005 275
            return self.wrap(_("Your counter proposal to an event invitation has been declined."), link=True)
paul@63 276
paul@63 277
    def publish(self):
paul@63 278
paul@139 279
        "Register details of any relevant event."
paul@63 280
paul@1005 281
        _ = self.get_translator()
paul@1005 282
paul@831 283
        if self._publish():
paul@1005 284
            return self.wrap(_("Details of an event have been received."))
paul@63 285
paul@63 286
    def refresh(self):
paul@63 287
paul@688 288
        "Requests to refresh events are handled either here or by the client."
paul@63 289
paul@1005 290
        _ = self.get_translator()
paul@1005 291
paul@688 292
        if self.is_refreshing():
paul@935 293
            return self._process(self._refresh, from_organiser=False)
paul@688 294
        else:
paul@1005 295
            return self.wrap(_("A request for updated event details has been received."))
paul@63 296
paul@61 297
    def reply(self):
paul@61 298
paul@61 299
        "Record replies and notify the recipient."
paul@61 300
paul@1005 301
        _ = self.get_translator()
paul@1005 302
paul@935 303
        if self._process(self._schedule_for_organiser, from_organiser=False):
paul@1005 304
            return self.wrap(_("A reply to an event invitation has been received."))
paul@61 305
paul@61 306
    def request(self):
paul@61 307
paul@61 308
        "Hold requests and notify the recipient."
paul@61 309
paul@1005 310
        _ = self.get_translator()
paul@1005 311
paul@935 312
        if self._process(self._schedule_for_attendee, queue=True):
paul@1005 313
            return self.wrap(_("An event invitation has been received."))
paul@60 314
paul@943 315
class Freebusy(CommonFreebusy, Handler):
paul@55 316
paul@55 317
    "A free/busy handler."
paul@55 318
paul@55 319
    def publish(self):
paul@63 320
paul@110 321
        "Register free/busy information."
paul@110 322
paul@1005 323
        _ = self.get_translator()
paul@1005 324
paul@180 325
        self._record_freebusy(from_organiser=True)
paul@180 326
paul@180 327
        # Produce a message if configured to do so.
paul@63 328
paul@468 329
        if self.is_notifying():
paul@1005 330
            return self.wrap(_("A free/busy update has been received."), link=False)
paul@55 331
paul@55 332
    def reply(self):
paul@55 333
paul@63 334
        "Record replies and notify the recipient."
paul@63 335
paul@1005 336
        _ = self.get_translator()
paul@1005 337
paul@180 338
        self._record_freebusy(from_organiser=False)
paul@180 339
paul@180 340
        # Produce a message if configured to do so.
paul@139 341
paul@468 342
        if self.is_notifying():
paul@1005 343
            return self.wrap(_("A reply to a free/busy request has been received."), link=False)
paul@55 344
paul@55 345
    def request(self):
paul@55 346
paul@55 347
        """
paul@55 348
        Respond to a request by preparing a reply containing free/busy
paul@468 349
        information for the recipient.
paul@55 350
        """
paul@55 351
paul@180 352
        # Produce a reply if configured to do so.
paul@55 353
paul@468 354
        if self.is_sharing():
paul@180 355
            return CommonFreebusy.request(self)
paul@55 356
paul@55 357
# Handler registry.
paul@55 358
paul@55 359
handlers = [
paul@55 360
    ("VFREEBUSY",   Freebusy),
paul@55 361
    ("VEVENT",      Event),
paul@55 362
    ]
paul@55 363
paul@55 364
# vim: tabstop=4 expandtab shiftwidth=4