imip-agent

Annotated imiptools/handlers/resource.py

1045:5a9722130d25
2016-02-08 Paul Boddie Used event-related attributes stored in the handler directly.
paul@48 1
#!/usr/bin/env python
paul@48 2
paul@48 3
"""
paul@48 4
Handlers for a resource.
paul@146 5
paul@1025 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@48 20
"""
paul@48 21
paul@737 22
from imiptools.data import get_address, to_part, uri_dict
paul@418 23
from imiptools.handlers import Handler
paul@683 24
from imiptools.handlers.common import CommonFreebusy, CommonEvent
paul@1039 25
from imiptools.handlers.scheduling import apply_scheduling_functions, \
paul@1040 26
                                          confirm_scheduling, \
paul@1040 27
                                          finish_scheduling, \
paul@1040 28
                                          retract_scheduling
paul@48 29
paul@725 30
class ResourceHandler(CommonEvent, Handler):
paul@131 31
paul@131 32
    "Handling mechanisms specific to resources."
paul@131 33
paul@937 34
    def _process(self, handle_for_attendee):
paul@131 35
paul@420 36
        """
paul@420 37
        Record details from the incoming message, using the given
paul@420 38
        'handle_for_attendee' callable to process any valid message
paul@420 39
        appropriately.
paul@420 40
        """
paul@420 41
paul@131 42
        oa = self.require_organiser_and_attendees()
paul@131 43
        if not oa:
paul@131 44
            return None
paul@131 45
paul@131 46
        organiser_item, attendees = oa
paul@131 47
paul@468 48
        # Process for the current user, a resource as attendee.
paul@131 49
paul@738 50
        if not self.have_new_object():
paul@468 51
            return None
paul@131 52
paul@468 53
        # Collect response objects produced when handling the request.
paul@131 54
paul@662 55
        handle_for_attendee()
paul@131 56
paul@676 57
    def _add_for_attendee(self):
paul@131 58
paul@420 59
        """
paul@676 60
        Attempt to add a recurrence to an existing object for the current user.
paul@676 61
        This does not request a response concerning participation, apparently.
paul@420 62
        """
paul@420 63
paul@737 64
        # Request details where configured, doing so for unknown objects anyway.
paul@676 65
paul@737 66
        if self.will_refresh():
paul@737 67
            self.make_refresh()
paul@676 68
            return
paul@676 69
paul@676 70
        # Record the event as a recurrence of the parent object.
paul@676 71
paul@676 72
        self.update_recurrenceid()
paul@737 73
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@676 74
paul@859 75
        # Remove any previous cancellations involving this event.
paul@859 76
paul@859 77
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 78
paul@676 79
        # Update free/busy information.
paul@676 80
paul@676 81
        self.update_event_in_freebusy(for_organiser=False)
paul@676 82
paul@1039 83
        # Confirm the scheduling of the recurrence.
paul@1039 84
paul@1039 85
        self.confirm_scheduling()
paul@1039 86
paul@676 87
    def _schedule_for_attendee(self):
paul@676 88
paul@676 89
        "Attempt to schedule the current object for the current user."
paul@676 90
paul@737 91
        attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user]
paul@1040 92
paul@1040 93
        # Attempt to schedule the event.
paul@1040 94
paul@936 95
        scheduled = self.schedule()
paul@655 96
paul@1040 97
        try:
paul@1040 98
            # Update the participation of the resource in the object.
paul@1040 99
            # Update free/busy information.
paul@662 100
paul@1040 101
            if scheduled in ("ACCEPTED", "DECLINED"):
paul@1040 102
                method = "REPLY"
paul@1040 103
                attendee_attr = self.update_participation(scheduled)
paul@662 104
paul@1040 105
                self.update_event_in_freebusy(for_organiser=False)
paul@1040 106
                self.remove_event_from_freebusy_offers()
paul@131 107
paul@1040 108
                # Set the complete event or an additional occurrence.
paul@334 109
paul@1040 110
                event = self.obj.to_node()
paul@1040 111
                self.store.set_event(self.user, self.uid, self.recurrenceid, event)
paul@381 112
paul@1040 113
                # Remove additional recurrences if handling a complete event.
paul@1040 114
                # Also remove any previous cancellations involving this event.
paul@381 115
paul@1040 116
                if not self.recurrenceid:
paul@1040 117
                    self.store.remove_recurrences(self.user, self.uid)
paul@1040 118
                    self.store.remove_cancellations(self.user, self.uid)
paul@1040 119
                else:
paul@1040 120
                    self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@361 121
paul@1040 122
                if scheduled == "ACCEPTED":
paul@1040 123
                    self.confirm_scheduling()
paul@1039 124
paul@1040 125
            # For countered proposals, record the offer in the resource's
paul@1040 126
            # free/busy collection.
paul@1039 127
paul@1040 128
            elif scheduled == "COUNTER":
paul@1040 129
                method = "COUNTER"
paul@1040 130
                self.update_event_in_freebusy_offers()
paul@1040 131
paul@1040 132
            # For inappropriate periods, reply declining participation.
paul@936 133
paul@1040 134
            else:
paul@1040 135
                method = "REPLY"
paul@1040 136
                attendee_attr = self.update_participation("DECLINED")
paul@936 137
paul@1040 138
        # Confirm any scheduling.
paul@936 139
paul@1040 140
        finally:
paul@1040 141
            self.finish_scheduling()
paul@936 142
paul@580 143
        # Make a version of the object with just this attendee, update the
paul@580 144
        # DTSTAMP in the response, and return the object for sending.
paul@574 145
paul@745 146
        self.update_sender(attendee_attr)
paul@574 147
        self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
paul@574 148
        self.update_dtstamp()
paul@662 149
        self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()]))
paul@131 150
paul@468 151
    def _cancel_for_attendee(self):
paul@131 152
paul@420 153
        """
paul@468 154
        Cancel for the current user their attendance of the event described by
paul@468 155
        the current object.
paul@420 156
        """
paul@420 157
paul@580 158
        # Update free/busy information.
paul@131 159
paul@580 160
        self.remove_event_from_freebusy()
paul@580 161
paul@672 162
        # Update the stored event and cancel it.
paul@672 163
paul@672 164
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@672 165
        self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@672 166
paul@1039 167
        # Retract the scheduling of the event.
paul@1039 168
paul@1039 169
        self.retract_scheduling()
paul@1039 170
paul@710 171
    def _revoke_for_attendee(self):
paul@710 172
paul@710 173
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@710 174
paul@710 175
        self.remove_event_from_freebusy_offers()
paul@710 176
paul@936 177
    # Scheduling details.
paul@936 178
paul@936 179
    def schedule(self):
paul@936 180
paul@936 181
        """
paul@936 182
        Attempt to schedule the current object, returning an indication of the
paul@936 183
        kind of response to be returned: "COUNTER" for counter-proposals,
paul@936 184
        "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for
paul@936 185
        invalid requests.
paul@936 186
        """
paul@936 187
paul@1025 188
        # Obtain a list of scheduling functions.
paul@936 189
paul@1025 190
        functions = self.get_preferences().get("scheduling_function",
paul@1025 191
            "schedule_in_freebusy").split("\n")
paul@936 192
paul@1025 193
        return apply_scheduling_functions(functions, self)
paul@936 194
paul@1039 195
    def confirm_scheduling(self):
paul@1039 196
paul@1039 197
        "Confirm that this event has been scheduled."
paul@1039 198
paul@1039 199
        functions = self.get_preferences().get("confirmation_function")
paul@1039 200
paul@1039 201
        if functions:
paul@1039 202
            confirm_scheduling(functions.split("\n"), self)
paul@1039 203
paul@1040 204
    def finish_scheduling(self):
paul@1040 205
paul@1040 206
        "Finish the scheduling, unlocking resources where appropriate."
paul@1040 207
paul@1040 208
        functions = self.get_preferences().get("scheduling_function",
paul@1040 209
            "schedule_in_freebusy").split("\n")
paul@1040 210
paul@1040 211
        finish_scheduling(functions, self)
paul@1040 212
paul@1039 213
    def retract_scheduling(self):
paul@1039 214
paul@1039 215
        "Retract this event from scheduling records."
paul@1039 216
paul@1039 217
        functions = self.get_preferences().get("retraction_function")
paul@1039 218
paul@1039 219
        if functions:
paul@1039 220
            retract_scheduling(functions.split("\n"), self)
paul@1039 221
paul@131 222
class Event(ResourceHandler):
paul@48 223
paul@48 224
    "An event handler."
paul@48 225
paul@48 226
    def add(self):
paul@676 227
paul@676 228
        "Add a new occurrence to an existing event."
paul@676 229
paul@937 230
        self._process(self._add_for_attendee)
paul@48 231
paul@48 232
    def cancel(self):
paul@131 233
paul@131 234
        "Cancel attendance for attendees."
paul@131 235
paul@937 236
        self._process(self._cancel_for_attendee)
paul@48 237
paul@48 238
    def counter(self):
paul@48 239
paul@48 240
        "Since this handler does not send requests, it will not handle replies."
paul@48 241
paul@48 242
        pass
paul@48 243
paul@48 244
    def declinecounter(self):
paul@48 245
paul@710 246
        "Revoke any counter-proposal."
paul@48 247
paul@937 248
        self._process(self._revoke_for_attendee)
paul@48 249
paul@48 250
    def publish(self):
paul@676 251
paul@676 252
        """
paul@676 253
        Resources only consider events sent as requests, not generally published
paul@676 254
        events.
paul@676 255
        """
paul@676 256
paul@48 257
        pass
paul@48 258
paul@48 259
    def refresh(self):
paul@626 260
paul@626 261
        """
paul@626 262
        Refresh messages are typically sent to event organisers, but resources
paul@626 263
        do not act as organisers themselves.
paul@626 264
        """
paul@48 265
paul@676 266
        pass
paul@676 267
paul@48 268
    def reply(self):
paul@48 269
paul@48 270
        "Since this handler does not send requests, it will not handle replies."
paul@48 271
paul@48 272
        pass
paul@48 273
paul@48 274
    def request(self):
paul@48 275
paul@48 276
        """
paul@48 277
        Respond to a request by preparing a reply containing accept/decline
paul@468 278
        information for the recipient.
paul@48 279
paul@48 280
        No support for countering requests is implemented.
paul@48 281
        """
paul@48 282
paul@937 283
        self._process(self._schedule_for_attendee)
paul@48 284
paul@725 285
class Freebusy(CommonFreebusy, Handler):
paul@48 286
paul@48 287
    "A free/busy handler."
paul@48 288
paul@48 289
    def publish(self):
paul@676 290
paul@676 291
        "Resources ignore generally published free/busy information."
paul@676 292
paul@943 293
        self._record_freebusy(from_organiser=True)
paul@48 294
paul@48 295
    def reply(self):
paul@48 296
paul@48 297
        "Since this handler does not send requests, it will not handle replies."
paul@48 298
paul@48 299
        pass
paul@48 300
paul@108 301
    # request provided by CommonFreeBusy.request
paul@48 302
paul@48 303
# Handler registry.
paul@48 304
paul@48 305
handlers = [
paul@48 306
    ("VFREEBUSY",   Freebusy),
paul@48 307
    ("VEVENT",      Event),
paul@48 308
    ]
paul@48 309
paul@48 310
# vim: tabstop=4 expandtab shiftwidth=4